In [ ]:
# --- HÀM TÍNH TOÁN CỐT LÕI ĐƯỢC TỐI ƯU VỚI NUMBA ---
@numba.jit(nopython=True, cache=True)
def macd_hvix_jit(returns, prices, time_series, p, ratio_1, ratio_2):
    """
    Hàm JIT-compiled để thực hiện tất cả các tính toán nặng.
    Chỉ làm việc với các mảng NumPy để đạt hiệu suất tối đa.
    """
    length = len(returns)
    num_time_series = len(time_series)
    
    # 1. Cấp phát trước các mảng kết quả để tr-ánh np.append
    all_stds = np.full((num_time_series, length), np.nan)
    all_alphas = np.full((num_time_series, length), np.nan)
    all_emas = np.full((num_time_series, length), np.nan)

    # 2. Vòng lặp tính STD, ALPHA, EMA cho mỗi time_series (ví dụ: 29 và 13)
    for i in range(num_time_series):
        time = time_series[i]
        
        # --- SỬA LỖI LOGIC TÍNH STD ---
        stds = np.full(length, np.nan)
        # Vòng lặp bắt đầu từ chỉ số `time-1` để có đủ dữ liệu cho cửa sổ đầu tiên
        for j in range(time - 1, length):
            # Cửa sổ này khớp với logic gốc: `returns[j-time+2 : j+1]`
            # Ví dụ: khi j = time-1, cửa sổ là returns[1:time]
            start_idx = j - time + 2
            end_idx = j + 1
            
            if start_idx < 0: continue # Đảm bảo chỉ số không âm
            
            window = returns[start_idx:end_idx]
            
            # Tính toán an toàn với NaN
            mean_window = np.nanmean(window)
            if not np.isnan(mean_window):
                stds[j] = np.nanmean(np.abs(window - mean_window))
        all_stds[i, :] = stds

        # Tính ALPHA
        alphas = np.full(length, np.nan)
        for j in range(time * 2 - 2, length):
            window_sum = np.nansum(stds[j - time + 1 : j + 1])
            if window_sum != 0:
                alphas[j] = stds[j] / window_sum
        all_alphas[i, :] = alphas

        # Tính EMA
        emas = np.full(length, np.nan)
        if length > time * 2 - 2:
            emas[time * 2 - 2] = prices[time * 2 - 2] # Giá trị khởi tạo
            for j in range(time * 2 - 2, length - 1):
                alpha_val = alphas[j]
                if not np.isnan(alpha_val):
                    emas[j+1] = (1 - alpha_val) * emas[j] + prices[j+1] * alpha_val
                else:
                    emas[j+1] = emas[j] # Giữ nguyên nếu alpha là NaN
        all_emas[i, :] = emas

    # 3. Tính DIFF
    diff = all_emas[0, :] - all_emas[1, :] # EMA_min - EMA_max

    # 4. Tính DEA
    dea = np.full(length, np.nan)
    max_time = time_series[1] # max_time là giá trị lớn hơn
    start_dea_idx = max_time * 2 + p - 2
    
    # Giá trị DEA khởi tạo
    initial_diff_window = diff[(max_time * 2-1) : (start_dea_idx+1)]
    dea[start_dea_idx] = np.mean(initial_diff_window)
    # Tính các giá trị DEA tiếp theo
    for j in range(start_dea_idx + 1, length):
        dea[j] = dea[j-1] * ratio_1 + diff[j] * ratio_2
    # 5. Tính OSC
    osc = diff - dea
    
    # Trả về tất cả các mảng đã tính toán
    return all_stds[1], all_emas[0], all_emas[1], diff, dea, osc


def macd_hvix_optimized(stock, pd_dataframe=True, brand=None, portfolio=None, price_type='AVE_PRICE'):
    """
    Hàm bao (wrapper) được tối ưu hóa.
    Nhiệm vụ: Chuẩn bị dữ liệu, gọi hàm JIT và lắp ráp kết quả.
    """
    global data_concat # Giả định data_concat tồn tại trong global scope
    global max_time

    data = data_concat if 'data_concat' in globals() else data_1

    # Các hằng số
    time_series = np.array([13, 29]) # min, max
    p = 9
    ratio_1 = 9.4 / 13
    ratio_2 = 1 - ratio_1
    
    stock = stock.upper() if pd_dataframe else stock
    
    # --- Bước 1: Chuẩn bị dữ liệu đầu vào ---
    if pd_dataframe:
        df_stock = data_1[stock]
        a_price = df_stock['AVE_PRICE'].values[::-1]
        m_price = df_stock['MODIFY_PRICE'].values[::-1]
        o_price = df_stock['OPEN'].values[::-1]
        h_price = df_stock['HIGH'].values[::-1]
        l_price = df_stock['LOW'].values[::-1]
        date = df_stock['DATE'].values[::-1]
        
        # Tính return một lần duy nhất
        log_price = np.log(a_price)
        # return_1 = np.full_like(log_price, np.nan)
        return_1 = np.diff(np.log(a_price),prepend=np.nan)
        
        df_input = pd.DataFrame({
            'DATE': date, 'AVE_PRICE': a_price, 'RETURN': return_1, 
            'MODIFY_PRICE': m_price, 'OPEN': o_price, 'HIGH': h_price, 'LOW': l_price
        })
    else:
        # Xử lý cho trường hợp đầu vào không phải là DataFrame
        df_stock = stock
        df_input = pd.DataFrame(df_stock)
        old_name = df_input.columns
        df_input.rename(columns={old_name[1]: 'AVE_PRICE', old_name[2]: 'RETURN'}, inplace=True)

    # Lọc ngày tháng
    start_date = pd.to_datetime('2014-03-31') if not portfolio else pd.to_datetime('2018-01-01')
    df = df_input[df_input['DATE'] > start_date].reset_index(drop=True)
    
    # Lấy các mảng NumPy để đưa vào hàm JIT
    returns_np = df['RETURN'].to_numpy(dtype=np.float32)
    prices_np = df['AVE_PRICE'].to_numpy(dtype=np.float32)

    # --- Bước 2: Gọi hàm JIT-compiled để thực hiện tính toán nặng ---
    std_max, ema_min, ema_max, diff, dea, osc = macd_hvix_jit(
        returns_np, prices_np, time_series, p, ratio_1, ratio_2
    )
    
    # --- Bước 3: Lắp ráp DataFrame kết quả ---
    df[f'STD_{time_series[1]}'] = std_max
    df[f'EMA_{time_series[0]}'] = ema_min
    df[f'EMA_{time_series[1]}'] = ema_max
    df['DIFF'] = diff
    df['DEA'] = dea
    df['OSC'] = osc

    # Định dạng lại DataFrame đầu ra theo yêu cầu
    if pd_dataframe:
        df_final = pd.DataFrame({
            'DATE': df['DATE'],
            'AVE_PRICE': df['AVE_PRICE'].round(3),
            'RETURN': df['RETURN'].round(3),
            'MODIFY_PRICE': df['MODIFY_PRICE'].round(3),
            f'STD_{time_series[1]}': df[f'STD_{time_series[1]}'].round(3),
            f'EMA_{time_series[0]}': df[f'EMA_{time_series[0]}'],
            f'EMA_{time_series[1]}': df[f'EMA_{time_series[1]}'],
            'DIFF': df['DIFF'].round(5),
            'DEA': df['DEA'].round(5),
            'OSC': df['OSC'].round(5),
            'OPEN': df['OPEN'].round(3),
            'LOW': df['LOW'].round(3),
            'HIGH': df['HIGH'].round(3)
        })
        df_final.columns = pd.MultiIndex.from_product([[stock.upper()], df_final.columns])
    else:
        df_final = pd.DataFrame({
            'DATE': df['DATE'],
            'AVE_PRICE': df['AVE_PRICE'].round(3),
            'RETURN': df['RETURN'].round(3),
            f'STD_{time_series[1]}': df[f'STD_{time_series[1]}'],
            'DIFF': df['DIFF'].round(5),
            'DEA': df['DEA'].round(5),
            'OSC': df['OSC'].round(5)
        })
        df_final.rename(columns={'AVE_PRICE': old_name[1], 'RETURN': old_name[2]}, inplace=True)
        df_final.columns = pd.MultiIndex.from_product([[brand.upper()], df_final.columns])

    return df_final
In [ ]:
def big_df_macd_jit(total_list, portfolio=False, drop_date=True, time_default=pdts('2020-02-13'), price_type='AVE_PRICE'):
    global column_macd
    column_macd = ['AVE_PRICE', 'RETURN', 'MODIFY_PRICE', f'STD_29', 'DIFF', 'DEA', 'OSC', 'OPEN', 'LOW', 'HIGH']
    
    all_macds = []
    for stock in total_list:
        macd = macd_hvix_optimized(stock, portfolio=portfolio, price_type=price_type)
        if drop_date:
             macd = macd[macd.iloc[:,0] > time_default].reset_index(drop=True).drop(columns = (stock.upper(), 'DATE'))
        else:
             macd = macd[macd.iloc[:,0] > time_default].reset_index(drop=True)
            
        all_macds.append(macd)
    
    df_macd = pd.concat(all_macds, axis=1)
    return df_macd
In [ ]:
def df_brand_index(dict_brand, price_type='AVE_PRICE'):
    vnindex_1 = vnindex[vnindex.iloc[:, 0] > pdts('2020-02-13')].reset_index(drop=True)
    vnindex_date = vnindex_1.iloc[:, 0].values[::-1]
    all_dfs = []
    
    for brand, list_stock in dict_brand.items():
        
        brand_market = {'INDEX_C': [], 'INDEX_O': [], 'INDEX_A': [], 'INDEX_H': [], 'INDEX_L': []}
        
        # Get issue of all stocks
        stock_issues = {stock: issue_1[stock.lower()].iloc[0,-1] for stock in list_stock}
        
        # Load all data before loop
        stock_data = {stock: data_1[stock.upper()] for stock in list_stock}
        
        for date in vnindex_date:
            brand_value_c = brand_value_o = brand_value_a = brand_value_h = brand_value_l = 0
            for stock in list_stock:
                data=stock_data[stock]
                date_index=np.where(data['DATE']==date)[0][0]
                c_price = data['MODIFY_PRICE'][date_index]
                o_price = data['OPEN'][date_index]
                
                if price_type == 'AVE_PRICE':
                    a_price = data['AVE_PRICE'][date_index]
                else:
                    prices_type = [data[type_p].values for type_p in price_type]
                    a_price = np.mean(prices_type, axis=0)[date_index]
                
                h_price = data['HIGH'][date_index]
                l_price = data['LOW'][date_index]
                issue = stock_issues[stock]
                
                brand_value_c += c_price * issue
                brand_value_o += o_price * issue
                brand_value_a += a_price * issue
                brand_value_h += h_price * issue
                brand_value_l += l_price * issue
                
            brand_market['INDEX_C'].append(brand_value_c)
            brand_market['INDEX_O'].append(brand_value_o)
            brand_market['INDEX_A'].append(brand_value_a)
            brand_market['INDEX_H'].append(brand_value_h)
            brand_market['INDEX_L'].append(brand_value_l)
        
        brand_index = pd.DataFrame(brand_market)
        
        brand_value_1 = brand_market['INDEX_C'][0]
        brand_value_2 = brand_market['INDEX_O'][0]
        brand_value_3 = brand_market['INDEX_A'][0]
        brand_value_4 = brand_market['INDEX_H'][0]
        brand_value_5 = brand_market['INDEX_L'][0]
       
        brand_index['INDEX_C'] = brand_index['INDEX_C'] / brand_value_1
        brand_index['INDEX_O'] = brand_index['INDEX_O'] / brand_value_2
        brand_index['INDEX_A'] = brand_index['INDEX_A'] / brand_value_3
        brand_index['INDEX_H'] = brand_index['INDEX_H'] / brand_value_4
        brand_index['INDEX_L'] = brand_index['INDEX_L'] / brand_value_5
        
        return_1=np.log(brand_index['INDEX_C']/brand_index['INDEX_C'].shift(1)).replace([np.inf,-np.inf],np.nan)
        return_2=np.log(brand_index['INDEX_O']/brand_index['INDEX_O'].shift(1)).replace([np.inf,-np.inf],np.nan)
        return_3=np.log(brand_index['INDEX_A']/brand_index['INDEX_A'].shift(1)).replace([np.inf,-np.inf],np.nan)
        return_4=np.log(brand_index['INDEX_H']/brand_index['INDEX_H'].shift(1)).replace([np.inf,-np.inf],np.nan)
        return_5=np.log(brand_index['INDEX_L']/brand_index['INDEX_L'].shift(1)).replace([np.inf,-np.inf],np.nan)
        
        brand_index['RETURN_C'] = return_1
        brand_index['RETURN_O'] = return_2
        brand_index['RETURN_A'] = return_3
        brand_index['RETURN_H'] = return_4
        brand_index['RETURN_L'] = return_5
        
        
        macd_brand_o=macd_hvix(pd.concat([vnindex_1.iloc[:,0],brand_index['INDEX_O'],brand_index['RETURN_O']],axis=1),
                              pd_dataframe=False,brand=brand)
        macd_brand_o.rename(columns={'DIFF':'DIFF_O','DEA':'DEA_O','OSC':'OSC_O'}, inplace=True)
        macd_brand_c=macd_hvix_optimized(pd.concat([vnindex_1.iloc[:,0],brand_index['INDEX_C'],brand_index['RETURN_C']],axis=1),
                              pd_dataframe=False,brand=brand)
        macd_brand_c.rename(columns={'DIFF':'DIFF_C','DEA':'DEA_C','OSC':'OSC_C'}, inplace=True)
        macd_brand_a=macd_hvix_optimized(pd.concat([vnindex_1.iloc[:,0],brand_index['INDEX_A'],brand_index['RETURN_A']],axis=1),
                              pd_dataframe=False,brand=brand)
        macd_brand_a.rename(columns={'DIFF':'DIFF_A','DEA':'DEA_A','OSC':'OSC_A'}, inplace=True)
        macd_brand_h=macd_hvix_optimized(pd.concat([vnindex_1.iloc[:,0],brand_index['INDEX_H'],brand_index['RETURN_H']],axis=1),
                              pd_dataframe=False,brand=brand)
        macd_brand_h.rename(columns={'DIFF':'DIFF_H','DEA':'DEA_H','OSC':'OSC_H'}, inplace=True)
        macd_brand_l=macd_hvix_optimized(pd.concat([vnindex_1.iloc[:,0],brand_index['INDEX_L'],brand_index['RETURN_L']],axis=1),
                              pd_dataframe=False,brand=brand)
        macd_brand_l.rename(columns={'DIFF':'DIFF_L','DEA':'DEA_L','OSC':'OSC_L'}, inplace=True)
        
        brand_index = pd.concat([brand_index['INDEX_O'], brand_index['RETURN_O'], macd_brand_o[brand.upper()]['DIFF_O'],
                                macd_brand_o[brand.upper()]['DEA_O'], macd_brand_o[brand.upper()]['OSC_O'],
                                brand_index['INDEX_C'], brand_index['RETURN_C'], macd_brand_c[brand.upper()]['DIFF_C'],
                                macd_brand_c[brand.upper()]['DEA_C'], macd_brand_c[brand.upper()]['OSC_C'],
                                brand_index['INDEX_A'], brand_index['RETURN_A'], macd_brand_a[brand.upper()]['DIFF_A'],
                                macd_brand_a[brand.upper()]['DEA_A'], macd_brand_a[brand.upper()]['OSC_A'],
                                brand_index['INDEX_H'], brand_index['RETURN_H'], macd_brand_h[brand.upper()]['DIFF_H'],
                                macd_brand_h[brand.upper()]['DEA_H'], macd_brand_h[brand.upper()]['OSC_H'],
                                brand_index['INDEX_L'], brand_index['RETURN_L'], macd_brand_l[brand.upper()]['DIFF_L'],
                                macd_brand_l[brand.upper()]['DEA_L'], macd_brand_l[brand.upper()]['OSC_L']
                                ], axis=1)
        
        brand_index.columns = pd.MultiIndex.from_product([[f'{brand.upper()}_INDEX'], brand_index.columns])
        all_dfs.append(brand_index)

    brands_index = pd.concat(all_dfs, axis=1)
    return brands_index

MAIN PROCESS¶

In [1]:
import warnings
warnings.filterwarnings("ignore", message="indexing past lexsort depth")
In [2]:
%run My_function.ipynb
C:\Users\duytk\anaconda3\envs\jupyterlab-debugger\Lib\site-packages\rpy2\robjects\packages.py:367: UserWarning: The symbol 'quartz' is not in this R namespace/package.
  warnings.warn(
invalid series dtype: expected `String`, got `time`
HBC
Time
invalid series dtype: expected `String`, got `time`
VNINDEX
Time
CPU times: total: 0 ns
Wall time: 0 ns
In [3]:
def df_score_estate():
    plan_detail = {
        'vic': {
            '2019': [pdts('2019-05-23'),1.1485, 1.0422],
            '2020': [pdts('2020-05-21'),1.1151, 0.648],
            '2021': [pdts('2021-05-16'),1.1150, 0.99],
            '2022': [pdts('2022-04-20'),1.115, 1.7938],
            '2023': [pdts('2023-05-17'),1.8665, 0.9784],
            '2024': [pdts('2024-04-25'),1.239, 2.1187],
            '2025': [pdts('2025-05-13'),1.59,1.9]
        },
        'vre':{
            '2019':[pdts('2019-03-30'),1.0413,1.1189],
            '2020':[pdts('2020-05-19'),1.0692,0.8769],
            '2021':[pdts('2021-05-17'),1.08,1.05],
            '2022':[pdts('2022-04-21'),1.358,1.825],
            '2023':[pdts('2023-04-19'),1.5623,1.8725],
            '2024':[pdts('2024-05-02'),0.97,1.0027],
            '2025':[pdts('2025-04-23'),1.06,1.15],
        },
        'nvl':{
            '2019':[pdts('2019-05-02'),1.17,1.007],
            '2020':[pdts('2020-05-20'),1.36,1.08],
            '2021':[pdts('2021-04-27'),5.447,1.05],
            '2022':[pdts('2022-05-18'),2.14,1.88],
            '2023':[pdts('2023-03-28'),0.85,0.1],
            '2024':[pdts('2024-04-08'),6.85,2.22],
            '2025':[pdts('2025-04-24'),1.315,0.9203]
            
        },
        'vhm':{
            '2019':[pdts('2019-05-23'),4.607,1.3951],
            '2020':[pdts('2020-05-20'),1.8789,1.2747],
            '2021':[pdts('2021-05-18'),1.2581,1.2409],
            '2022':[pdts('2022-04-21'),0.8829,0.7702],
            '2023':[pdts('2023-04-25'),1.6028,1.0288],
            '2024':[pdts('2024-04-24'),1.1588,1.0438],
            '2025':[pdts('2025-04-23'),1.76,1.2]
        },
        'kbc':{
            '2019':[pdts('2019-04-10'),1.5656,1.2821],
            '2020':[pdts('2020-06-30'),1.0595,0.8731],
            '2021':[pdts('2021-04-10'),3.0698,6.2696],
            '2022':[pdts('2022-01-22'),2.3086,3.3309],
            '2023':[pdts('2022-12-14'),9.4737,2.5381],
            '2024':[pdts('2024-03-08'),1.6020,1.7817],
            '2025':[pdts('2025-02-14'),3,7]
        },
        'dxg':{
            '2019':[pdts('2019-03-27'),1.0764,1.0183],
            '2020':[pdts('2020-05-25'),0.84,0.85],
            '2021':[pdts('2021-06-10'),3.11,3.72],
            '2022':[pdts('2022-03-28'),1.1,1.21],
            '2023':[pdts('2023-04-21'),0.999,0.7353],
            '2024':[pdts('2024-04-01'),1.05,1.31],
            '2025':[pdts('2025-04-28'),1.46,1.44]
        },
        'pdr':{
            '2019':[pdts('2019-03-30'),3.19,1.2442],
            '2020':[pdts('2020-04-25'),1.1144,1.3574],
            '2021':[pdts('2021-03-30'),1.2017,1.5311],
            '2022':[pdts('2022-07-03'),2.9558,1.5634],
            '2023':[pdts('2023-07-02'),1.8617,0.5862],
            '2024':[pdts('2024-04-09'),4.829,1.2893],
            '2025':[pdts('2025-12-30')]
        },
        'nlg':{
            '2019':[pdts('2019-04-19'),1.0015,1.1297],
            '2020':[pdts('2020-04-23'),0.5959,0.81],
            '2021':[pdts('2021-04-23'),2.24,1.61],
            '2022':[pdts('2022-04-23'),1.37,1.13],
            '2023':[pdts('2023-04-23'),1.1148,1.0624],
            '2024':[pdts('2024-04-23'),2.0927,1.0263],
            '2025':[pdts('2025-04-16'),0.9441,1.35]
        },
        'kdh':{
            '2019':[pdts('2019-04-12'),1.0288,1.11],
            '2020':[pdts('2020-06-01'),1.244,1.2],
            '2021':[pdts('2021-04-10'),1.0591,1.0419],
            '2022':[pdts('2022-04-04'),1.07,1.16],
            '2023':[pdts('2023-04-06'),1.0649,0.9070],
            '2024':[pdts('2024-04-20'),1.8687,1.0836],
            '2025':[pdts('2025-04-20'),1.16,1.23]
        },
        # CTR 2019 is at UPCOM
        'ctr':{
            '2019':[pdts('2019-06-01'),1.159,1.077],
            '2020':[pdts('2020-05-04'),1.18,1.1],
            '2021':[pdts('2021-04-02'),1.034,1.006],
            '2022':[pdts('2022-04-10'),1.15,1.1],
            '2023':[pdts('2023-04-03'),1.1034,1.1065],
            '2024':[pdts('2024-04-09'),1.11,1.041],
            '2025':[pdts('2025-03-27'),1.103,1.074]
        },
        # Not determine the growth plan of CII at 2019
        'cii':{
            '2019':[pdts('2019-12-30'),2.0104,3.3329],
            '2020':[pdts('2020-03-12'),3.1991,1.5509],
            '2021':[pdts('2021-03-04'),1.2467,1.1065],
            '2022':[pdts('2022-04-04'),2.8,3.2801],
            '2023':[pdts('2023-04-08'),0.69,0.64],
            '2024':[pdts('2024-04-03'),0.897,1.1],
            '2025':[pdts('2025-03-28'),0.9,0.63],
        },
        'vgc':{
            '2019':[pdts('2019-06-18'),1.055,1.122],
            '2020':[pdts('2020-06-03'),0.818,0.773],
            '2021':[pdts('2021-03-30'),1.27,1.19],
            '2022':[pdts('2022-04-30'),1.34,1.1],
            '2023':[pdts('2023-03-24'),1.09,0.56],
            '2024':[pdts('2024-02-16'),1.021,0.76],
            '2025':[pdts('2025-03-14'),1.21,1.07]
        },
        'vcg':{
            '2019':[pdts('2019-06-25'),1,1.16],
            '2020':[pdts('2020-06-19'),1.002,1.04],
            '2021':[pdts('2021-04-25'),1.41,0.6],
            '2022':[pdts('2022-04-07'),2.43,2.69],
            '2023':[pdts('2023-03-25'),1.7,0.92],
            '2024':[pdts('2024-04-07'),1.15,2.4],
            '2025':[pdts('2025-04-02'),1.18,1.08]
        },
        'ctd':{
            '2019':[pdts('2019-03-29'),0.945,0.861],
            '2020':[pdts('2020-06-15'),0.67,0.84],
            '2021':[pdts('2021-04-05'),1.2,1.02],
            '2022':[pdts('2022-04-05'),1.65,0.83],
            '2023':[pdts('2023-04-15'),1.22, 4.40],
            '2024':[],
            '2025':[],
        }
    }

    audited_time = {
        'nvl': {
            'DATE': [
                pdts('2020-02-14'),
                pdts('2020-02-27'),
                pdts('2020-08-21'),
                pdts('2021-03-12'),
                pdts('2021-08-26'),
                pdts('2022-03-04'),
                pdts('2022-08-26'),
                pdts('2023-04-20'),
                pdts('2023-09-06'),
                pdts('2024-04-03'),
                pdts('2024-09-30'),
                pdts('2025-04-02')
            ],
            'SCORE_AUDIT': [0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -2, -1],
            'NOTE_AUDIT': [
                'FIRST TIME', 'NO COMMENT', 'NO COMMENT', 'NO COMMENT',
                'NO COMMENT', 'NO COMMENT', 'NO COMMENT',
                'Profit decreased by more than 110 billion and accountants emphasize the ability to continue operations',
                'Accountants emphasize the ability to continue operations',
                'Accountants emphasize the ability to continue operations',
                'Profit decreased by more than 7,300 billion and accountants really doubted the ability to continue operations because of many impossible assumption about continuing operations ',
                'Accountants emphasize the ability to continue operations'
            ]
        },
        'vhm': {
            'DATE': [
                pdts('2020-02-14'),
                pdts('2020-04-02'),
                pdts('2020-09-07'),
                pdts('2021-04-02'),
                pdts('2021-08-09'),
                pdts('2022-04-04'),
                pdts('2022-09-03'),
                pdts('2023-03-17'),
                pdts('2023-09-01'),
                pdts('2024-03-18'),
                pdts('2024-09-02'),
                pdts('2025-03-30')
            ],
            'SCORE_AUDIT': [0]*12,
            'NOTE_AUDIT': ['FIRST TIME']+['NO COMMENT'] * 11
        },
        'vre': {
            'DATE': [
                pdts('2020-02-14'),
                pdts('2020-04-22'),
                pdts('2020-09-03'),
                pdts('2021-04-02'),
                pdts('2021-08-30'),
                pdts('2022-04-04'),
                pdts('2022-08-24'),
                pdts('2023-03-09'),
                pdts('2023-08-25'),
                pdts('2024-04-01'),
                pdts('2024-09-09'),
                pdts('2025-04-13')
            ],
            'SCORE_AUDIT': [0] * 12,
            'NOTE_AUDIT': ['FIRST TIME'] + ['NO COMMENT'] * 11
        },
        'vic': {
            'DATE': [
                pdts('2020-02-14'),
                pdts('2020-04-08'),
                pdts('2020-09-03'),
                pdts('2021-04-02'),
                pdts('2021-09-02'),
                pdts('2022-04-04'),
                pdts('2022-09-02'),
                pdts('2023-03-06'),
                pdts('2023-09-04'),
                pdts('2024-04-26'),
                pdts('2024-09-02'),
                pdts('2025-04-02')
                
            ],
            'SCORE_AUDIT': [0] * 12,
            'NOTE_AUDIT': ['FIRST TIME'] + ['NO COMMENT'] * 4 +
            ['INCREASED UNDISTRIBUTION PROFIT'] + ['NO COMMENT'] * 6
        },
        'kbc': {
            'DATE': [
                pdts('2020-02-14'),
                pdts('2020-04-02'),
                pdts('2020-09-02'),
                pdts('2021-04-05'),
                pdts('2021-09-20'),
                pdts('2022-03-23'),
                pdts('2022-09-05'),
                pdts('2023-04-03'),
                pdts('2023-08-25'),
                pdts('2024-04-03'),
                pdts('2024-08-28'),
                pdts('2025-04-03')
            ],
            'SCORE_AUDIT': [0, 0, 0, 0, 0, 0, -2, 0, -0.5, -0.5, 0, 0],
            'NOTE_AUDIT': ['FIRST TIME'] + ['NO COMMENT'] +
            ['DECREASED PROFIT OF UNCONTROL SHAREHOLDER'] + ['NO COMMENT'] * 3 +
            ['Profit decreased by more than 2000 billion'] + ['NO COMMENT'] +
            ['Profit increased by more than 250 billion'] +
            ['Profit of unaudit report decreased by more than 150 billion'] +
            ['NO COMMENT']*2
        },
        'dxg':{
            'DATE':[pdts('2020-02-14'),
                    pdts('2020-02-17'),pdts('2020-08-21'),
                    pdts('2021-03-22'),pdts('2021-08-04'),
                    pdts('2022-03-16'),pdts('2022-09-05'),
                    pdts('2023-04-05'),pdts('2023-08-11'),
                    pdts('2024-03-14'),pdts('2024-08-09'),
                    pdts('2025-04-03')
                   ],
            'SCORE_AUDIT':[0]*3 + [-1] + [0]*8,
            'NOTE_AUDIT':['FIRST TIME']+['NO COMMENT']*2+['Profit decreased by 64 billion']+['NO COMMENT']*8   
        },
        'pdr':{
            'DATE':[pdts('2020-02-14'),
                    pdts('2020-03-09'),pdts('2020-08-17'),
                    pdts('2021-03-08'),pdts('2021-08-16'),
                    pdts('2022-03-03'),pdts('2022-08-15'),
                    pdts('2023-04-05'),pdts('2023-08-28'),
                    pdts('2024-03-07'),pdts('2024-09-04'),
                    pdts('2025-04-07')
                   ],
            'SCORE_AUDIT':[0,0,0,0,0,0,0,-0.5,0,0,0,0],
            'NOTE_AUDIT':['FIRST TIME'] + ['NO COMMENT']*6+['Sold Địa ốc Hòa Bình subsidiary after only one week of holding and lost more than 200 billion'] + 
                         ['NO COMMENT']*4
        },
        'nlg':{
            'DATE':[pdts('2020-02-14'),
                    pdts('2020-04-06'),pdts('2020-09-14'),
                    pdts('2021-03-21'),pdts('2021-08-19'),
                    pdts('2022-03-21'),pdts('2022-08-17'),
                    pdts('2023-04-02'),pdts('2023-08-21'),
                    pdts('2024-03-22'),pdts('2024-08-23'),
                    pdts('2025-03-20'),pdts('2025-08-06')
                   ],
            'SCORE_AUDIT':[0]*13,
            'NOTE_AUDIT':['FIRST TIME'] + ['NO COMMENT']*12
        },
        'kdh':{
            'DATE':[pdts('2020-02-14'),
                    pdts('2020-03-30'),pdts('2020-08-23'),
                    pdts('2021-03-25'),pdts('2021-08-23'),
                    pdts('2022-03-23'),pdts('2022-08-29'),
                    pdts('2023-03-27'),pdts('2023-09-05'),
                    pdts('2024-03-25'),pdts('2024-08-19'),
                    pdts('2025-03-27')
                   ],
            'SCORE_AUDIT':[0]*12,
            'NOTE_AUDIT':['FIRST TIME'] + ['NO COMMENT']*11
        },
        'ctr':{
            'DATE':[pdts('2020-02-14'),
                    pdts('2020-04-02'),pdts('2020-09-07'),
                    pdts('2021-04-07'),pdts('2021-09-20'),
                    pdts('2022-04-04'),pdts('2022-09-05'),
                    pdts('2023-04-03'),pdts('2023-09-05'),
                    pdts('2024-03-28'),pdts('2024-09-04'),
                    pdts('2025-03-23')
                   ],
            'SCORE_AUDIT':[0]*12,
            'NOTE_AUDIT':['FIRST TIME'] + ['NO COMMENT']*11
        },
        'cii':{
            'DATE':[pdts('2020-02-14'),
                    pdts('2020-04-13'),pdts('2020-09-04'),
                    pdts('2021-03-19'),pdts('2021-10-04'),
                    pdts('2022-03-21'),pdts('2022-08-29'),
                    pdts('2023-04-03'),pdts('2023-08-28'),
                    pdts('2024-03-07'),pdts('2024-08-12'),
                    pdts('2025-03-07'),pdts('2025-07-30'),
                   ],
            'SCORE_AUDIT':[0]*13,
            'NOTE_AUDIT':['FIRST TIME']+['NO COMMENT']*12
        },
        'vgc':{
            'DATE':[pdts('2020-02-14'),
                    pdts('2020-03-09'),pdts('2020-09-14'),
                    pdts('2021-03-18'),pdts('2021-09-16'),
                    pdts('2022-03-09'),pdts('2022-08-15'),
                    pdts('2023-03-17'),pdts('2023-08-21'),
                    pdts('2024-03-08'),pdts('2024-08-19'),
                    pdts('2025-02-24')
                   ],
            'SCORE_AUDIT':[0]*12,
            'NOTE_AUDIT':['FIRST TIME'] + ['NO COMMENT']*11
        },
        'vcg':{
            'DATE':[pdts('2020-02-14'),
                    pdts('2020-04-09'),pdts('2020-09-06'),
                    pdts('2021-04-05'),pdts('2021-09-08'),
                    pdts('2022-04-04'),pdts('2022-09-07'),
                    pdts('2023-03-31'),pdts('2023-09-05'),
                    pdts('2024-04-26'),pdts('2024-09-04'), 
                    pdts('2025-04-25')
                    
                   ],
            'SCORE_AUDIT':[0]*4+[-0.25]+[0]*7,
            'NOTE_AUDIT':['FIRST TIME']+['NO COMMENT']*3+['Profit decreased after audit by 30 billion']+['NO COMMENT']*7
        },
        'ctd':{
            'DATE':[pdts('2020-02-14'),
                    pdts('2020-04-01'),pdts('2020-08-31'),
                    pdts('2021-04-05'),pdts('2021-09-01'),
                    pdts('2022-04-03'),pdts('2022-09-05'),
                    pdts('2023-04-03'),pdts('2023-10-02'),
                    pdts('2024-03-03'),pdts('2024-09-30'),
                    pdts('2025-03-06')
                   ],
            'SCORE_AUDIT':[0]*3+[-2]+[0]*8,
            'NOTE_AUDIT':['FIRST TIME']+['NO COMMENT']*2+['Profit decreased after audit by more 128 billion']+['NO COMMENT']*8,
        }
    }


    date_col=vnindex[vnindex.loc[:,'DATE']>pdts('2020-02-13')].sort_values('DATE',ascending=True,ignore_index=True)['DATE']
    df_1=pd.DataFrame(date_col)
    df_1=pd.concat([df_1],keys=['DATE'],axis=1)
    for stock in estate_con:
        stock=stock.lower()
        score = {
            'DATE': [],
            'SCORE': [],
            'SCORE_REV': [],
            'SCORE_CASH': [],
            'SCORE_LIQ': [],
            'SCORE_CREDIT': [],
            'SCORE_DIVIDEND': [],
            'NOTE':[]
        }

        # Get all data of vcb
        # No need to compare with other, just need truely good finance
        report_time = report_times[stock]
        data = data_1[stock.upper()]
        balance = reports_1[stock][0].reset_index(drop=True)
        income = reports_1[stock][1]
        cash_flow = reports_1[stock][2]
        events = events_2[stock]
        issue = issue_1[stock]
        stock_plan = plan_detail[stock]

        # Dates of dividend being cash
        cash_point = events['DATE']

        sta_fil = data[data['DATE'] > pdts(year=2020, month=2, day=13)]
        sta_loca = sta_fil.iloc[-1].name

        length_time = len(report_time)
        iter_report = iter(report_time[1:])
        iter_report_1 = iter(report_time)

        i = 0
        for period in range(0, length_time, 4):
            quarter_point = {
                '4': next(iter_report_1, report_time[-1] + DateOffset(months=4)),
                '1': next(iter_report_1, report_time[-1] + DateOffset(months=4)),
                '2': next(iter_report_1, report_time[-1] + DateOffset(months=4)),
                '3': next(iter_report_1, report_time[-1] + DateOffset(months=4))
            }
            for quarter, quarter_time in quarter_point.items():
                # print(quarter)
                # print(quarter_time)
                quarter = int(quarter)
                quarter_1 = next(iter_report, report_time[-1] + DateOffset(months=4))
                year = quarter_time.year if quarter != 4 else quarter_time.year-1
                # grow_plan = stock_plan[str(year)] if quarter != 1 else [1.2, 1.2]
                grow_before_plan = stock_plan[str(year)]
                if (stock =='ctd') and (quarter_1.date() >= pdts('2023-11-03').date()):    
                    growth_plan_ctd_adj = {'2023':[pdts('2023-10-19'),1.16,4.051],
                                           '2024':[pdts('2024-10-19'),1.188,1.387],
                                           '2025':[pdts('2025-10-19')]
                                          }
                    year = quarter_time.year if quarter == 3 else quarter_time.year-1
                    grow_before_plan = growth_plan_ctd_adj[str(year)]
                grow_plan = grow_before_plan if grow_before_plan[0] <= quarter_time  else [0,1.2,1.2]
                time_2_score=f'Q{quarter} {year}'
                if time_2_score not in income.columns:break
                # 1/ Score COMPLETED REVENUE
                item_str='Doanh số thuần'
                if income.at[2,'ITEM'] == item_str:
                    revenue = income.at[2, f'Q{quarter} {year}']
                    revenue_before = income.at[2, f'Q{quarter} {year-1}']
                else:
                    index_item=income[income.loc[:,'ITEM']==item_str].index[0]
                    revenue = income.at[index_item, f'Q{quarter} {year}']
                    revenue_before = income.at[index_item, f'Q{quarter} {year-1}']

                item_str='Lãi/(lỗ) thuần sau thuế'
                if income.at[20,'ITEM'] == item_str:
                    profit = income.at[20, f'Q{quarter} {year}']
                    profit_before = income.at[20, f'Q{quarter} {year-1}']
                else:
                    index_item=income[income.loc[:,'ITEM']==item_str].index[0]
                    profit = income.at[index_item, f'Q{quarter} {year}']
                    profit_before = income.at[index_item, f'Q{quarter} {year-1}']

                completed_pro = profit > grow_plan[2] * 0.95 * profit_before
                completed_rev = revenue > grow_plan[1] * 0.95 * revenue_before

                if completed_pro & completed_rev:
                    score_revenue = 1
                elif completed_pro | completed_rev:
                    score_revenue = 0.5
                else:
                    score_revenue = 0
                # 2/ Score CASH FLOW operation

                col_cashflow = list(cash_flow.columns)
                index_col = col_cashflow.index(f'Q{quarter} {year}')
                item_str='Lưu chuyển tiền thuần từ các hoạt động sản xuất kinh doanh'
                # Because of no standard item column
                if cash_flow.loc[0,'ITEM'] == item_str:
                    operation = cash_flow.iloc[0,
                                               (index_col - 2):(index_col + 1)].mean() > 0
                    operation_now = cash_flow.iloc[0, index_col] > 0
                else:
                    index_item=cash_flow[cash_flow.loc[:,'ITEM']==item_str].index[0]
                    operation = cash_flow.iloc[index_item,
                                               (index_col - 2):(index_col + 1)].mean() > 0
                    operation_now = cash_flow.iloc[index_item, index_col] > 0


                if operation & operation_now:
                    score_cash = 1
                elif operation:
                    score_cash = 0.75
                elif operation_now:
                    score_cash = 0.25
                else:
                    score_cash = 0

                # 3/ Score CASH BEFORE/INTEREST PAID
                item_str='Tiền và tương đương tiền cuối kỳ'
                item_str_1='Chi phí lãi vay đã trả'
                if (cash_flow.iat[-1,2] == item_str) & (cash_flow.iat[17,2] == item_str_1):
                    liq = cash_flow.iat[-1, index_col - 1] +cash_flow.iat[17, index_col] > cash_flow.iat[-1, index_col-2]+cash_flow.iat[17, index_col - 1] > cash_flow.iat[-1, index_col - 3] + cash_flow.iat[17, index_col - 2]
                    liq_1 = cash_flow.iat[-1, index_col - 1] + cash_flow.iat[17, index_col] > cash_flow.iat[-1, index_col - 2]+cash_flow.iat[17, index_col-1]
                else:
                    index_item=cash_flow[cash_flow.loc[:,'ITEM']==item_str].index[0]
                    index_item_1=cash_flow[cash_flow.loc[:,'ITEM']==item_str_1].index[0]
                    liq = cash_flow.iat[index_item, index_col - 1]+cash_flow.iat[index_item_1, index_col] > cash_flow.iat[index_item, index_col -2] +cash_flow.iat[index_item_1, index_col - 1] > cash_flow.iat[index_item, index_col - 3] +cash_flow.iat[index_item_1, index_col - 2]
                    liq_1 = cash_flow.iat[index_item, index_col - 1]+cash_flow.iat[index_item_1, index_col] > cash_flow.iat[index_item, index_col - 2]+cash_flow.iat[index_item_1, index_col-1]

                if liq:
                    score_liq = 1
                elif liq_1:
                    score_liq = 0.5
                else:
                    score_liq = 0

                # 4/ CREDIT CAPACITY
                item_str='Tiền thu được các khoản đi vay'
                item_str_1='Tiển trả các khoản đi vay'
                if (cash_flow.at[32,'ITEM'] == item_str) & (cash_flow.at[33,'ITEM'] == item_str):
                    credit_cap = cash_flow.iloc[32:34, index_col].sum() > 0
                    credit_cap_1 = cash_flow.iloc[32:34, index_col - 1].sum() > 0
                    credit_cap_2 = cash_flow.iloc[32:34, index_col - 2].sum() > 0
                else:
                    index_item=cash_flow[cash_flow.loc[:,'ITEM']==item_str].index[0]
                    index_item_1=cash_flow[cash_flow.loc[:,'ITEM']==item_str_1].index[0]
                    credit_cap = cash_flow.iloc[index_item:index_item_1+1, index_col].sum() > 0
                    credit_cap_1 = cash_flow.iloc[index_item:index_item_1+1, index_col - 1].sum() > 0
                    credit_cap_2 = cash_flow.iloc[index_item:index_item_1+1, index_col - 2].sum() > 0

                if credit_cap & credit_cap_1 & credit_cap_2:
                    score_credit = 1
                elif (credit_cap & credit_cap_1) | (credit_cap & credit_cap_2):
                    score_credit = 0.5
                elif credit_cap_1 & credit_cap_2:
                    score_credit = 0.25
                elif credit_cap | credit_cap_1 | credit_cap_2:
                    score_credit = 0
                else:
                    score_credit = -1
                # 5/ DIVIDEND
                while (sta_fil.iat[sta_loca - i, 0] <= quarter_1) & (sta_loca - i >= 0):
                    begin_dividend = pdts(
                        year=int(sta_fil.iat[sta_loca - i, 0].year) - 3,
                        month=1,
                        day=1)
                    range_dividend = pd.date_range(end=sta_fil.iat[sta_loca - i, 0],
                                                   start=begin_dividend,
                                                   freq='D')
                    df_dividend = (
                        events[events['DATE'].isin(range_dividend)]).reset_index(
                            drop=True)
                    cash_count = df_dividend[df_dividend['TYPE'].str.contains('Cash')]

                    if len(cash_count) >= 2: 
                        score_dividend = 1
                    elif len(cash_count) == 1:
                        score_dividend = 0.5
                    else:
                        score_dividend = -1

                    if score_dividend == score_credit == -1:
                        note='NO CASH DIVIDEND, BAD CREDIT'
                    elif score_dividend == -1:
                        note='NO CASH DIVIDEND'
                    elif score_credit == -1:
                        note='BAD CREDIT'
                    else:
                        note='NO COMMENT'
                    score['DATE'].append(sta_fil.iat[sta_loca-i,0])
                    score['SCORE_CASH'].append(score_cash)
                    score['SCORE_LIQ'].append(score_liq)
                    score['SCORE_CREDIT'].append(score_credit)
                    score['SCORE_REV'].append(score_revenue)
                    score['SCORE_DIVIDEND'].append(score_dividend)
                    score['SCORE'].append(score_revenue+score_cash+score_liq+score_credit+score_dividend)
                    score['NOTE'].append(note)
                    i+=1
        df_score=pd.DataFrame(score)


        df_audit = pd.DataFrame(audited_time[stock])


        i = 0
        y = 0
        score_audit = []
        note_audit = []
        length_score=len(df_score)

        while i < len(df_audit):
            date_audit = df_audit['DATE'][i]
            try:
                date_audit_1 = df_audit['DATE'][i + 1]
            except:
                date_audit_1 = df_audit['DATE'][i] + DateOffset(months=9)
            if y == length_score:break
            else:date_score = df_score.at[y,'DATE']
            if date_audit <= date_score <= date_audit_1:
                score_audit.append(df_audit['SCORE_AUDIT'][i])
                note_audit.append(df_audit['NOTE_AUDIT'][i])
                y+=1
            else:
                i+=1

        df_score['SCORE_AUDIT']=pd.Series(score_audit)
        df_score['NOTE_AUDIT']=pd.Series(note_audit)

        df_score.loc[:,'SCORE']=df_score.loc[:,'SCORE'].add(df_score.loc[:,'SCORE_AUDIT'])
        score_summary=pd.concat([df_score['SCORE'],df_score['NOTE'],df_score['NOTE_AUDIT']],axis=1)
        score_summary.columns= pd.MultiIndex.from_product([[stock.upper()],list(score_summary.columns)])
        df_1=pd.concat([df_1,score_summary],axis=1)
    return df_1
In [4]:
def score_retail(stock):
    #Revenue and profit in plan_detail
    # Every growth plan of mwg always announce before quarter 1 of year
    # Every growth plan of frt always announce before quarter 1 of year except 2019,2020
    # Every growth plan of mwg always announce before quarter 1 of year
    plan_detail={'mwg':{'2019':[pdts('2018-12-25'),1.2362,1.24],'2020':[pdts('2020-02-04'),1.1983,1.2604],'2021':[pdts('2020-12-22'),1.1518,1.212],'2022':[pdts('2022-01-29'),1.1386,1.2957],'2023':[pdts('2023-02-17'),1.068,1.085],'2024':[pdts('2024-02-06'),1.06,14.31],'2025':[pdts('2025-02-05'),1.12,1.3]},   
                 'frt':{'2019':[pdts('2019-04-26'),1.16,1.20],'2020':[pdts('2020-05-29'),0.92,0.81],'2021':[pdts('2021-04-06'),1.12,4.286],'2022':[pdts('2022-03-15'),1.2,1.3],'2023':[pdts('2023-03-26'),1.13,0.49],'2024':[pdts('2024-03-28'),1.17,2.288],'2025':[pdts('2025-02-25'),1.2,1.71]},
                 'dgw':{'2019':[pdts('2019-04-25'),1.2,1.25],'2020':[pdts('2020-04-08'),1.2016,1.2379],'2021':[pdts('2021-04-20'),1.2125,1.1221],'2022':[pdts('2022-03-15'),1.257,1.2215],'2023':[pdts('2023-03-29'),0.9079,0.5851],'2024':[pdts('2024-04-01'),1.22,1.38],'2025':[pdts('2025-03-31'),1.15,1.18]}
                }

    score={'DATE':[],'TOTAL_SCORE':[],'SCORE_1':[],'SCORE_2':[],'SCORE_3':[],'SCORE_4':[],'SCORE_5':[],
          'SCORE_6':[],'SCORE_7':[],'SCORE_8':[],'SCORE_9':[]}
    box_score={'SCORE':[]}
    # Get all data of vcb
    report_time=report_times[stock]
    data=data_1[stock.upper()]
    balance=reports_1[stock][0].reset_index(drop=True)
    profit=reports_1[stock][1]
    cash_flow=reports_1[stock][2]
    events=events_2[stock]
    issue=issue_1[stock]
    stock_plan=plan_detail[stock]
    events_1=events[(events['DATE']>pdts(year=2020,month=2,day=13))&(events['TYPE'].str.contains('Cash'))]
    # Dates of dividend being cash
    cash_point=events['DATE']
    # Define the location at the study point (2016)
    #stu_fil = data[data['DATE']>pdts(year=2017,month=1,day=1)]
    #stu_loca=stu_fil.iloc[-1].name+1
    #stu_ave=stu_fil.iloc[(stu_loca-10):(stu_loca),3].mean()
    # Define the location at the start point (2020)
    sta_fil=data[data['DATE']>pdts(year=2020,month=2,day=13)]
    sta_loca=sta_fil.iloc[-1].name+1
    sta_ave=sta_fil.iloc[(sta_loca-10):(sta_loca),3].mean()
    i=1
    j=0
    try:
        index_start=data.loc[data['DATE']==report_time[0]].index[0]
    except:
        index_start=data.loc[data['DATE']>report_time[0]].index[-1]
    capital_market=data.iloc[(index_start+10):(index_start+1),3].mean()*issue.iloc[0,-1]
    # DUPLICATE_CODE
    begin_dividend=pdts(year=int(sta_fil.iloc[sta_loca-i,0].year)-2,month=1,day=1)
    range_dividend=pd.date_range(end=sta_fil.iloc[sta_loca-i,0],start=begin_dividend,freq='D')
    df_dividend=(events[events['DATE'].isin(range_dividend)]).reset_index(drop=True)
    cash_count=df_dividend['TYPE'].str.contains('Cash').count()
    good_dividend=True if cash_count >=2 else False
    cash_index=df_dividend['TYPE'].str.contains('Cash').index
    for index in cash_index:
        if 'Cash' in df_dividend.iloc[index,1]:
            break
    pattern = r'[-+]?\d*\.\d+|\d+'
    # Dividend cash into foramt int
    cash_price = float(re.findall(pattern,df_dividend.iloc[index,1])[0])*0.1
    # Location of this dividend in data
    row_dividend = data.loc[data['DATE']==df_dividend.iloc[index,0]].index[0]
    # Average price from 10 period nearest
    price_market = data.iloc[(row_dividend-10):(row_dividend+1),3].mean()
    # D/P index
    d_p=cash_price/price_market
    #DUPLICATE_CODE
    length_time=len(report_time)
    iter_report=iter(report_time[1:])
    iter_report_1=iter(report_time)
    for period in range(0,length_time,4):
        quarter_point={'4':next(iter_report_1,report_time[-1]+DateOffset(months=4)),
                      '1':next(iter_report_1,report_time[-1]+DateOffset(months=4)),
                      '2':next(iter_report_1,report_time[-1]+DateOffset(months=4)),
                      '3':next(iter_report_1,report_time[-1]+DateOffset(months=4))}
        for quarter, quarter_time in quarter_point.items():
    #         if int(quarter)==1 and quarter_time.year>2020:
    #             continue
            quarter=int(quarter)
            quarter_1= next(iter_report,report_time[-1]+DateOffset(months=4))
            year=quarter_time.year

            grow_plan=stock_plan[str(year)] if quarter!=4 else stock_plan[str(year-1)]  
            #REVENUE
            try:
                code_10=profit.loc[2,f'Q{quarter} {year}' if quarter!=4 else f'Q{quarter} {year-1}']
            except:
                break
            # Use the `eq()` method to create a boolean DataFrame
            bool_df = profit.eq(code_10)
            # Find column names where the target value matches
            matching_columns = bool_df.columns[bool_df.any()]
            # Find the index of the column by name
            column_quarter = profit.columns.get_loc(matching_columns[0])
            code_10_before = profit.iloc[2,column_quarter-4]
            grow_code_10= code_10 > (code_10_before*grow_plan[1])
            grow_code_10_1 = code_10 > code_10_before
            #PROFIT AFTER TAX
            code_60=profit.iloc[20,column_quarter]
            code_60_before=profit.iloc[20,column_quarter-4]
            grow_code_60= code_60 > (code_60_before*grow_plan[2])
    #SCORE_1
            score_1=0
            # Grow more than plan 5% so plus 1.05
            if code_10 > code_10_before*grow_plan[1]*1.05:
                score_1=1.5
            # Grow more than plan so plus 1 
            elif grow_code_10:
                score_1=1 
            elif (code_10 > code_10_before*grow_plan[1]*0.95):
                score_1=0.5
            # Smaller plan 90% so minus -1
            elif (code_10 < code_10_before*grow_plan[1]*0.9) or (code_10 < code_10_before):
                score_1=-1
            # Smaller plan 95% but bigger 90% so minus 0.5
            elif code_10_before*grow_plan[1]*0.9 < code_10 < code_10_before*grow_plan[1]*0.95:
                score_1=-0.5
    #SCORE_2
            score_2=0
            code_140=balance.iloc[18,column_quarter]
            code_140_1=balance.iloc[18,column_quarter-1]
            ave_inv=(code_140_1+code_140)*0.5
            code_220=balance.iloc[36,column_quarter]
            code_220_1=balance.iloc[36,column_quarter-1]
            ave_fix_asset=(code_220+code_220_1)*0.5
            productive_rev=code_10/(ave_inv+ave_fix_asset)
            code_140_1=balance.iloc[18,column_quarter-4]
            code_140_2=balance.iloc[18,column_quarter-5]
            ave_inv_before=(code_140_1+code_140_2)*0.5
            code_220_1=balance.iloc[36,column_quarter-4]
            code_220_2=balance.iloc[36,column_quarter-5]
            ave_fix_asset_before=(code_220_1+code_220_2)*0.5
            productive_rev_before=code_10_before/(ave_inv_before+ave_fix_asset_before)
            compare_pro = productive_rev > productive_rev_before
            if grow_code_10 and compare_pro:
                score_2=1.5
            elif grow_code_10_1 and compare_pro:
                score_2=0.5        
            elif (code_10 < code_10_before) and (productive_rev < productive_rev_before):
                score_2=-1.5
            elif grow_code_10_1 and (productive_rev < productive_rev_before):
                score_2=-0.5
    #SCORE_3
            score_3=0
            code_22=profit.iloc[6,column_quarter]
            code_22_1=profit.iloc[6,column_quarter-4]
            code_25=profit.iloc[9,column_quarter]
            code_25_1=profit.iloc[9,column_quarter-4]
            code_26=profit.iloc[10,column_quarter]
            code_26_1=profit.iloc[10,column_quarter-4]
            total_expense=code_22+code_25+code_26
            total_expense_1=code_22_1+code_25_1+code_26_1
            compare_22=code_22_1/code_10_before > code_22/code_10
            compare_25_10= code_25/code_25_1 > code_10/code_10_before
            compare_25=code_25/code_10 < code_25_1/code_10_before
            compare_total=total_expense/code_10 > total_expense/code_10_before
            if grow_code_10 and compare_total:
                score_3=1.5
            elif grow_code_10_1 and compare_25:
                socre_3=0.5
            elif (code_10 < code_10_before) and compare_total:
                socre_3=-1.5
            elif compare_25_10:
                score_3=-0.5
    #SCORE_5
            score_5=0
            index_320=balance.loc[balance['CODE']==320].index[0]
            index_338=balance.loc[balance['CODE']==338].index[0]
            index_400=balance.loc[balance['CODE']==400].index[0]
            code_320=balance.iloc[index_320,column_quarter]
            code_320_1=balance.iloc[index_320,column_quarter-4]
            code_338=balance.iloc[index_338,column_quarter]
            code_338_1=balance.iloc[index_338,column_quarter-4]
            code_400=balance.iloc[index_400,column_quarter]
            code_400_1=balance.iloc[index_400,column_quarter-4]
            compare_str_asset= (code_320+code_338)/code_400 > (code_320_1+code_338_1)/code_400_1
            acc_quarter=quarter
            code_20_cash=0
            if acc_quarter == 4: 
                last_year=True
            while acc_quarter > 1:
                code_20_cash+=cash_flow.loc[0,f'Q{acc_quarter} {year-1}' if last_year else f'Q{acc_quarter} {year}']
                acc_quarter-=1
            last_year=False
            compare_20_cash=code_20_cash > 0
            if compare_str_asset and compare_20_cash and grow_code_10:
                score_5=1.5
            elif grow_code_10_1 and compare_20_cash and compare_str_asset:
                score_5=1
    #SCORE_6
            score_6=0
            code_14=cash_flow.iloc[9,column_quarter]
            ave_code_14=cash_flow.iloc[9,(column_quarter-20):(column_quarter)][3:].mean()
            deviation=np.mean(np.absolute(cash_flow.iloc[9,(column_quarter-20):(column_quarter)][3:]
                                                      .sub(ave_code_14)))
            #low_bound=ave_return-np.std(df.loc[(j-max_time-1):(j-1),'RETURN']) standard deviation
            low_code_14=ave_code_14-deviation
            high_code_14=ave_code_14+deviation
            #code_14 < 0
            compare_14 = high_code_14 > code_14 > low_code_14
            compare_14_low = code_14 > high_code_14
            if compare_14 and grow_code_10 and grow_code_60:
                score_6=1
            elif compare_14 and grow_code_10:
                score_6=0.5
            elif compare_14_low and grow_code_10 == False:
                score_6=-1
            elif compare_14_low:
                score_6=-0.5
    #SCORE_7
            score_7=0
            code_32=cash_flow.iloc[32,column_quarter]
            code_33=cash_flow.iloc[33,column_quarter]
            compare_32_33=(code_32+code_33) > 0 
            code_32_1=cash_flow.iloc[32,column_quarter-1]
            code_33_1=cash_flow.iloc[33,column_quarter-1]
            compare_32_33_1=(code_32_1+code_33_1) > 0 
            code_20=cash_flow.iloc[0,column_quarter]
            code_20_1=cash_flow.iloc[0,column_quarter-1]
            compare_20 = code_20 > 0
            compare_20_1 = code_20_1 > 0 
            ave_code_14_1=cash_flow.iloc[17,(column_quarter-5):(column_quarter)].mean(axis=0)
            compare_14_ave=code_14 > ave_code_14_1
            if compare_32_33 and compare_20 and compare_14_ave:
                score_7=1.5
            elif compare_32_33 and compare_20:
                score_7=0.75
            elif compare_32_33 and grow_code_10_1:
                score_7=0.5            
            elif compare_32_33 == False and compare_32_33_1 == False and compare_20 == False and compare_20_1 == False:
                score_7=-1
    #SCORE_8
            score_8=0
            roic=code_60/(balance.iloc[[92,75,88],column_quarter].sum(axis=0)-
                                                   balance.iloc[2,column_quarter])
            roic_1=profit.iloc[20,column_quarter-1]/(balance.iloc[[92,75,88],column_quarter-1].sum(axis=0)-
                                                   balance.iloc[2,column_quarter-1])
            roic_2=profit.iloc[20,column_quarter-2]/(balance.iloc[[92,75,88],column_quarter-2].sum(axis=0)-
                                                   balance.iloc[2,column_quarter-2])
            roic_3=profit.iloc[20,column_quarter-3]/(balance.iloc[[92,75,88],column_quarter-3].sum(axis=0)-
                                                   balance.iloc[2,column_quarter-3])
            roic_4=profit.iloc[20,column_quarter-4]/(balance.iloc[[92,75,88],column_quarter-4].sum(axis=0)-
                                                   balance.iloc[2,column_quarter-4])
            roic_5=profit.iloc[20,column_quarter-5]/(balance.iloc[[92,75,88],column_quarter-5].sum(axis=0)-
                                                   balance.iloc[2,column_quarter-5])
            compare_roic_ave = roic > np.mean([roic_1,roic_2,roic_3,roic_4,roic_5])
            compare_roic     = roic > roic_1
            compare_roic_12  = pd.Series([roic,roic_1,roic_2,roic_3,roic_4,roic_5]).all() > 0.12
            compare_roic_1   = roic < roic_1 < roic_2
            compare_roic_2   = roic < roic_1 < roic_2 < roic_3 < roic_4
            if compare_roic_ave and compare_roic_12 and compare_roic:
                score_8=1.5
            elif compare_roic and compare_roic_12:
                score_8=0.75
            elif (roic < 0.12) or compare_roic_2:
                score_8=-1
            elif compare_roic_1 and compare_roic_12:
                score_8=-0.5
    #SCORE_9_1
            if i==1:
                try:
                    index_start=data.loc[data['DATE']==report_time[0]].index[0]
                except:
                    index_start=data.loc[data['DATE']>report_time[0]].index[-1]
                capital_market=data.iloc[(index_start+10):(index_start+1),3].mean()*issue.iloc[0,-1]
            else:
                capital_market=sta_fil.iloc[(sta_loca-i+10):(sta_loca-i+1),3].mean()*issue.iloc[0,-1]
            ev_1=balance.iloc[64,column_quarter]-balance.iloc[2,column_quarter]
            ev_2=ev_1+capital_market
            ebitda=profit.iloc[[16,7],column_quarter].sum()+cash_flow.iloc[3,column_quarter]
            while (sta_fil.iloc[sta_loca-i,0] <= quarter_1) & (sta_loca-i>=0):
    #SCORE_4
                try:
                    price_market=sta_fil.iloc[(sta_loca-i):(sta_loca-i+11),3].mean()
                    price_market_1=sta_fil.iloc[(sta_loca-i+240):(sta_loca-i+251),3].mean()
                except:
                    price_market=data.iloc[(sta_loca-i):(sta_loca-i+11),3].mean()
                    price_market_1=data.iloc[(sta_loca-i+240):(sta_loca-i+251),3].mean()
                grow_price=price_market/price_market_1-1
                good_grow = (d_p+grow_price) > 0.15
                bad_grow  = (d_p+grow_price) < 0.1
                score_4=0
                if sta_fil.iloc[sta_loca-i,0] in cash_point:
                    begin_dividend=pdts(year=int(sta_fil.iloc[sta_loca-i,0].year)-2,month=1,day=1)
                    range_dividend=pd.date_range(end=sta_fil.iloc[sta_loca-i,0],start=begin_dividend,freq='D')
                    df_dividend=(events[events['DATE'].isin(range_dividend)]).reset_index(drop=True)
                    cash_count=df_dividend['TYPE'].str.contains('Cash')
                    good_dividend=True if len(cash_count) >=2 else False
                    cash_index=df_dividend['TYPE'].str.contains('Cash').index
                    for index in cash_index:
                        if 'Cash' in df_dividend.iloc[index,1]:
                            break
                    pattern = r'[-+]?\d*\.\d+|\d+'
                    # Dividend cash into foramt int
                    cash_price = float(re.findall(pattern,df_dividend.iloc[index,1])[0])*0.1
                    # Location of this dividend in data
                    row_dividend = data.loc[data['DATE']==df_dividend.iloc[index,0]].index[0]
                    # Average price from 10 period nearest
                    price_market = data.iloc[(row_dividend+10):(row_dividend+1),3].mean()
                    # D/P index
                    d_p=cash_price/price_market
                    good_grow = (d_p+grow_price) > 0.15
                    bad_grow  = (d_p+grow_price) < 0.1
                if good_dividend and good_grow:
                    score_4=1
                elif good_grow:
                    score_4=0.5
                elif bad_grow and not good_dividend:
                    score_4=-1
                elif bad_grow and good_dividend:
                    score_4=-0.5
    #SCORE_9_2
                score_9=0
                try:
                    capital_market=sta_fil.iloc[(sta_loca-i+10):(sta_loca-i+1),3].mean()*issue.iloc[0,-1]
                except:
                    capital_market=data.iloc[(sta_loca-i+10):(sta_loca-i+1),3].mean()*issue.iloc[0,-1]
                ev_3=ev_1+capital_market
                ev_ebitda=ev_3/ebitda
                ev_ebitda_1=ev_2/ebitda
                compare_ev= ev_ebitda > ev_ebitda_1
                if compare_ev and grow_code_60 and compare_roic:
                    score_9=1
                elif grow_code_60 and compare_roic:
                    score_9=0.5
                total_score=sum([score_1,score_2,score_3,score_4,score_5,score_6,score_7,score_8,score_9])
                score['DATE'].append(sta_fil.iloc[sta_loca-i,0])
                score['TOTAL_SCORE'].append(total_score)
                score['SCORE_1'].append(score_1)
                score['SCORE_2'].append(score_2)
                score['SCORE_3'].append(score_3)
                score['SCORE_4'].append(score_4)
                score['SCORE_5'].append(score_5)
                score['SCORE_6'].append(score_6)
                score['SCORE_7'].append(score_7)
                score['SCORE_8'].append(score_8)
                score['SCORE_9'].append(score_9)
                box_score['SCORE'].append(total_score)
                i+=1
                j+=1
    df_score_1=pd.DataFrame(score)
    df_score=pd.DataFrame(data=box_score)
    df_score.columns=pd.MultiIndex.from_product([[stock.upper()], df_score.columns])
    return df_score
In [5]:
# from pandas import Timestamp as pdts
df=total_df(bank_stock,retail_stock,sec_stock,index_funds)
In [6]:
# import polars as pl
# import pandas as pd

# Giả sử các biến file_pattern_1, all_stock, và df đã được định nghĩa
file_pattern_1 = "C:/Users/duytk/CF_1m/*.parquet"
cf_1m = pl.scan_parquet(file_pattern_1)
unique_symbols = cf_1m.select(pl.col("Symbol").unique()).collect().to_series()


all_stock = bank_stock + sec_stock + estate_con_stock + retail_stock
all_stock = [stock.upper() for stock in all_stock]
# all_stock = [...]
# df = pd.DataFrame(...) # df của bạn có cột 'DATE'

# --- BƯỚC 1: LẤY TẤT CẢ DỮ LIỆU CẦN THIẾT TRONG MỘT LẦN TRUY VẤN BẰNG POLARS ---

# Yêu cầu Polars lọc tất cả các mã cổ phiếu bạn cần trong một lần quét duy nhất
# và chỉ lấy các cột cần thiết.
all_infi_data = cf_1m.filter(
    pl.col("Symbol").is_in(all_stock)
).select(
    ["Symbol", "Date", "INFI", "INFI_SMA25"]
).collect() # Thực thi truy vấn và lấy dữ liệu vào bộ nhớ

# Chuyển kết quả sang Pandas DataFrame để dễ dàng thao tác với df gốc
# Đảm bảo cột 'Date' có cùng kiểu dữ liệu datetime
all_infi_data_pd = all_infi_data.to_pandas()
all_infi_data_pd['Date'] = pd.to_datetime(all_infi_data_pd['Date']).dt.date
df['DATE_ONLY'] = pd.to_datetime(df['DATE'].values.ravel().astype('datetime64[D]'))# Tạo cột date để join
df['DATE_ONLY'] = df['DATE_ONLY'].dt.date

# --- BƯỚC 2: GÁN DỮ LIỆU VÀO DATAFRAME PANDAS MỘT CÁCH HIỆU QUẢ ---

# Pivot dữ liệu để mỗi mã cổ phiếu có các cột INFI và INFI_SMA25 riêng
pivoted_data = all_infi_data_pd.pivot(index='Date', columns='Symbol', values=['INFI', 'INFI_SMA25'])

# Đổi tên cột của DataFrame đã pivot để khớp với cấu trúc MultiIndex của bạn
# Ví dụ: ('INFI', 'ACB') -> ('ACB', 'INFI')
pivoted_data.columns = pivoted_data.columns.swaplevel(0, 1)

# Sử dụng merge để kết hợp dữ liệu, đây là cách làm hiệu quả nhất
# Dùng 'left' merge để giữ tất cả các ngày từ df gốc
df = pd.merge(df, pivoted_data, left_on='DATE_ONLY', right_index=True, how='left')

# Xóa cột tạm và fill các giá trị NaN bằng 0 nếu cần
df = df.drop(columns=['DATE_ONLY'])
# Lọc ra các cột INFI để fillna, tránh ảnh hưởng các cột khác
infi_cols = [col for col in df.columns if 'INFI' in col[1]]
df[infi_cols] = df[infi_cols].fillna(0)
In [7]:
brands_1 = {'BANK_INDEX':[stock.upper() for stock in bank_stock],
          'SEC_INDEX':[stock.upper() for stock in sec_stock],
          'RETAIL_INDEX':[stock.upper() for stock in retail_stock],
          'ESTA_CON_INDEX':[stock.upper() for stock in estate_con_stock]
        }
for brand_stock, list_stock in brands_1.items():
    new_col = (brand_stock,f'{brand_stock[:-5]}INFI')
    col_infi_brand = [(stock, 'INFI') for stock in list_stock]
    df[new_col] = df[col_infi_brand].sum(axis=1)
In [8]:
# --- GIẢ ĐỊNH ---
# Các biến sau cần được định nghĩa trong môi trường của bạn trước khi khởi tạo class:
# df: DataFrame chính chứa toàn bộ dữ liệu.
# bank_stock, retail_stock, sec_stock, estate_con_stock: List các mã cổ phiếu theo ngành.
# date_begin: Ngày bắt đầu mô phỏng.
# box_2: Dữ liệu box_1 đã được lưu trước đó (nếu có).
# date_public: Dict chứa ngày công bố báo cáo tài chính.
# stock_dividend: Dict chứa DataFrame thông tin cổ tức cho mỗi cổ phiếu.
# max_time: Số ngày quá khứ cần thiết để tính toán chỉ báo.
# --- TỐI ƯU HÓA VỚI NUMBA ---
# Các hàm tính toán cốt lõi được đưa ra ngoài và "trang trí" bằng @numba.jit
# để được dịch sang mã máy tốc độ cao.
@numba.jit(nopython=True, cache=True)
def conditions_jit(stock_data_slice, max_time, diff_loc, dea_loc):
    """
    Phiên bản JIT-compiled hiệu suất cao, thay thế các hàm NumPy bằng vòng lặp
    để tránh lỗi 'contiguous array' và tăng tốc độ tối đa.
    """
    # Lấy ra các lát cắt dữ liệu cần thiết (dạng view, không tốn bộ nhớ)
    diff_slice = stock_data_slice[max_time-3:, diff_loc]
    dea_slice = stock_data_slice[max_time-7:, dea_loc]
    infi_slice = stock_data_slice[-1,-1]
    col2_data = stock_data_slice[:, 2]


    # --- TỐI ƯU HÓA: Tự tính diff bằng phép trừ vector hóa, Numba xử lý rất tốt ---
    # Cách này tránh được lỗi 'contiguous array' mà không cần dùng .copy()
    diff_of_diff_slice = diff_slice[1:] - diff_slice[:-1]
    diff_of_dea_slice = dea_slice[1:] - dea_slice[:-1]

    # --- Tính toán điều kiện 'diff' và 'dea' bằng NumPy ---
    diff_cond = np.all(diff_of_diff_slice > -0.0008) 
    dea_cond = np.all(diff_of_dea_slice > -0.0008) & np.any(diff_of_diff_slice > 0) & (infi_slice > -1)

    # --- Tự triển khai logic so sánh giá, xử lý NaN an toàn ---
    compare_val = stock_data_slice[max_time, 2]
    low_val = stock_data_slice[max_time, 4]
    
    compare_cond = True if compare_val > (np.mean(col2_data) - low_val) * 1.02 else False
    
    return (diff_cond  | dea_cond)  & compare_cond
@numba.jit(nopython=True, cache=True)
def _loop_precompute(stock_np,max_time, start_idx, diff_loc, dea_loc):
    """
    JIT-compiled vòng lặp chính của _precompute_conditions.
    """
    buy_signals = np.zeros(len(stock_np), dtype=numba.boolean)
    for i in numba.prange(max(start_idx, max_time), len(stock_np)):buy_signals[i] = conditions_jit(stock_np[i - max_time : i + 1], max_time, diff_loc, dea_loc)
    return buy_signals

@numba.jit(nopython=True, cache=True)
def _check_all_diff_jit(arr_slice, mode:np.int8, threshold=0.0):
    """
    Hàm trợ giúp JIT-compiled, được tối ưu để kiểm tra các điều kiện trên một mảng sai phân.
    """
        
    diff_arr = arr_slice[1:] - arr_slice[:-1]
    
    if mode == 0:  # Chế độ ">" (lớn hơn)
        for x in diff_arr: 
            if x <= threshold: return False
    else:  # Chế độ "<" (nhỏ hơn)
        for x in diff_arr:
            if x >= threshold: return False
                
    return True

@numba.jit(nopython=True, cache=True)
def calculate_industry_conditions_jit(index_day, sec_diffs_a, sec_oscs_a, bank_diffs_a, esta_diffs_a, retail_diffs_a):
    """
    Hàm JIT-compiled để tính toán 6 điều kiện ổn định của ngành.
    Luôn trả về một tuple 6 booleans.
    """
    
    return (_check_all_diff_jit(sec_diffs_a[index_day-4:index_day+1], mode=0), _check_all_diff_jit(sec_oscs_a[index_day-4:index_day+1], mode=1),
            _check_all_diff_jit(bank_diffs_a[index_day-6:index_day+1], mode=1), _check_all_diff_jit(esta_diffs_a[index_day-4:index_day+1], mode=0), 
            _check_all_diff_jit(bank_diffs_a[index_day-4:index_day+1], mode=0), _check_all_diff_jit(retail_diffs_a[index_day-4:index_day+1], mode=0))

@numba.jit(nopython=True, cache=True)
def check_unstable_idx_jit(bank_diffs_a_1, sec_oscs_a_1, custom_threshold=0.0005):
    """
    Hàm JIT-compiled riêng để kiểm tra điều kiện bất ổn tùy chỉnh.
    Luôn trả về một boolean duy nhất.
    """
    
    return _check_all_diff_jit(bank_diffs_a_1, mode=1, threshold=custom_threshold) & _check_all_diff_jit(sec_oscs_a_1, mode=1, threshold=custom_threshold)
    
    

@numba.jit(nopython=True, cache=True)
def mae_jit(data_slice,bound:np.float32):
    """
    Phiên bản JIT-compiled của hàm mae để tính toán hiệu suất cao.
    Sử dụng các hàm NumPy được Numba hỗ trợ.
    """
    # np.nanmean của (hiệu số tuyệt đối)
    return np.mean(np.abs(data_slice - np.mean(data_slice))) <= bound



    
class OptimizedStockTradeManager:
    """
    Phiên bản tối ưu và hoàn chỉnh của StockTradeManager.
    Tập trung vào vector hóa, tính toán trước và truy cập dữ liệu hiệu quả
    để tăng tốc độ xử lý backtest.
    """
    def __init__(self, df, bank_stock, retail_stock, sec_stock, estate_con_stock, date_begin, box_2):
        self.estate_con_stock=[stock.upper() for stock in estate_con_stock]
        self.df = df
        self.bank_stock = [stock.upper() for stock in bank_stock]
        self.retail_stock = [stock.upper() for stock in retail_stock]
        self.sec_stock = [stock.upper() for stock in sec_stock]
        self.all_brands = {'bank': self.bank_stock, 'retail': self.retail_stock, 'sec': self.sec_stock, 'estate': self.estate_con_stock}

        self.date_begin = date_begin
        self.box_2 = box_2
        self.length = len(df)
        self.max_time = 29

        # --- Tối ưu hóa: Chuyển đổi dữ liệu sang cấu trúc truy cập nhanh hơn ---
        all_stock_symbols = list(df.columns.get_level_values(0).unique())
        self.data_numpy = {
            stock: df[stock].to_numpy(dtype=np.float32)
            for stock in all_stock_symbols if stock != 'DATE'
        }
        self.dates_array = df['DATE'].values.ravel().astype('datetime64[D]')
        self.start_idx = np.searchsorted(self.dates_array, self.date_begin)

        # --- Tối ưu hóa: Lấy vị trí cột một lần duy nhất ---
        sample_stock_cols = df[self.bank_stock[0]].columns
        self.open_loc = sample_stock_cols.get_loc('OPEN')
        self.high_loc = sample_stock_cols.get_loc('HIGH')
        self.low_loc = sample_stock_cols.get_loc('LOW')
        self.modify_loc = sample_stock_cols.get_loc('MODIFY_PRICE')
        self.ave_loc = sample_stock_cols.get_loc('AVE_PRICE')
        self.score_loc = sample_stock_cols.get_loc('SCORE')
        self.diff_loc = sample_stock_cols.get_loc('DIFF')
        self.dea_loc = sample_stock_cols.get_loc('DEA')
        self.osc_loc = sample_stock_cols.get_loc('OSC')
        # self.infi_sma25 = sample_stock_cols.get_loc('INFI_SMA25')
        # --- Tối ưu hóa: Pre-fetch các cột beta ---
        self.beta_locs = {}
        for brand_stocks in self.all_brands.values():
            for stock in brand_stocks:
                self.beta_locs[stock] = df[stock].columns.get_loc(f'BETA_{stock}')
        # Khởi tạo các box lưu trữ
        self.box_1 = {date: {} for date in df['DATE'].iloc[:, 0]}
        self.box_trades = {
            f'box_trade_{i+1}': {
                'DATE': [self.date_begin], 'STOCK': [None], 'BUY_DATE': [None],
                'BUY_PRICE': [None], 'CHECK_DATE': [None], 'BAD_SIGN':[None],
                'SELL_DATE': [None], 'SELL_DATE_1': [None], 'SELL_DATE_2': [None],
                'SELL_PRICE': [None], 'SELL_PRICE_1': [None], 'SELL_PRICE_2': [None],
                'SELL_PRICE_WEIGHT': [None], 'PROFIT': [None], 'CUM_PROFIT': [1],
            } for i in range(5)
        }

    def rank_stocks(self, not_replace=True):
        """
        Tối ưu hóa việc xếp hạng cổ phiếu bằng cách vector hóa.
        """
        if not not_replace:
            self.box_1 = self.box_2
            betas_sec = pd.DataFrame(
                {s: self.data_numpy[s][:, self.beta_locs[s]] for s in self.sec_stock}, 
                index=self.dates_array
            )
            ranks_sec = np.argsort(betas_sec.values,axis=1) # Beta từ thấp đến cao
            ranked_cols = betas_sec.columns.to_numpy()
            
            for i, date in enumerate(self.df['DATE'].iloc[:, 0]):
                 self.box_1[date]['sec'] = ranked_cols[ranks_sec[i]].tolist()
            return self.box_1

        # --- Trường hợp not_replace=True ---
        for brand, stocks in self.all_brands.items():
            if not stocks: continue
            
            # --- SỬA LỖI LOGIC: Xác định đúng quy tắc xếp hạng cho mỗi ngành ---
            # Ngành 'sec' và 'estate' xếp hạng tăng dần (giá trị thấp tốt hơn)
            # Các ngành còn lại xếp hạng giảm dần (giá trị cao tốt hơn)
            ascending = brand in ['sec', 'estate']
            
            if brand == 'sec':
                data_to_rank = {s: self.data_numpy[s][:, self.beta_locs[s]] for s in stocks if s in self.beta_locs}
            else: # Các ngành khác đều dùng SCORE
                data_to_rank = {s: self.data_numpy[s][:, self.score_loc] for s in stocks}

            if not data_to_rank: continue
            
            df_to_rank = pd.DataFrame(data_to_rank, index=self.dates_array)
            ranks = np.argsort(np.argsort(df_to_rank.values, axis=1))
            
            if not ascending:
                # Đảo ngược rank cho các trường hợp xếp hạng giảm dần
                ranks = len(df_to_rank.columns) - 1 - ranks

            ranked_cols = df_to_rank.columns.to_numpy()
            for i, date in enumerate(self.df['DATE'].iloc[:, 0]):
                self.box_1[date][brand] = ranked_cols[ranks[i]].tolist()
        
        return self.box_1
    
    def _replace_score(self, brand):
        """
        Thay thế điểm theo logic đã định nghĩa. Tối ưu bằng cách tính toán trước index.
        """
        stock_chosen = self.all_brands.get(brand)
        if not stock_chosen: return
        
        stock_public = date_public.get(brand, {})
        
        for quarter, date_range in stock_public.items():
            if quarter == 'Q4 2019' or not date_range: continue
            
            start_date = pd.to_datetime(date_range[0]).date()
            
            # Tìm ngày thứ hai tuần tới
            first_public_idx = np.searchsorted(self.dates_array, start_date)
            if first_public_idx >= self.length: continue
            
            first_public_date = pd.to_datetime(self.dates_array[first_public_idx])
            monday_offset = (7 - first_public_date.weekday()) % 7
            effective_date = first_public_date + pd.Timedelta(days=monday_offset)
            start_idx = np.searchsorted(self.dates_array, effective_date.date())

            # Tìm ngày kết thúc
            end_date = pd.to_datetime(date_range[1]) if len(date_range) > 1 and pd.notna(date_range[1]) else self.dates_array[min(start_idx + 20, self.length - 1)]
            end_idx = np.searchsorted(self.dates_array, end_date.date(), side='right')
            
            if start_idx >= end_idx: continue

            # Lấy beta và xếp hạng cho khoảng thời gian đó
            betas = pd.DataFrame(
                {s: self.data_numpy[s][start_idx:end_idx, self.beta_locs[s]] for s in stock_chosen if s in self.beta_locs},
                index=pd.to_datetime(self.dates_array[start_idx:end_idx])
            )
            # Xếp hạng giảm dần theo beta
            ranks = np.argsort(np.argsort(betas.values, axis=1)) # Beta được sắp xếp từ cao xuống thấp
            ranked_cols = betas.columns.to_numpy()

            for i, date in enumerate(betas.index):
                self.box_1[date][brand] = ranked_cols[ranks[i]].tolist()

    def _avoid_dividend(self, brand):
        """
        Tối ưu hóa việc tránh cổ tức bằng cách tính toán trước và dùng set.
        """
        stock_list = self.all_brands.get(brand)
        if not stock_list: return

        last_day = self.dates_array[-1]
        avoid_dates_map = {}

        for stock in stock_list:
            dividend_df_subset = stock_dividend.get(stock)
            # if dividend_df_subset is None or dividend_df.empty: continue

            # Chỉ xét 10 sự kiện đầu tiên để khớp logic gốc
            # check_len = min(10, len(dividend_df))
            # dividend_df_subset = dividend_df

            valid_dividends = dividend_df_subset[
                (dividend_df_subset['notifyEvent'].values.ravel().astype('datetime64[D]') >= self.date_begin) & 
                (dividend_df_subset['notifyEvent'].values.ravel().astype('datetime64[D]') <= last_day)
            ]
            
            avoid_set = set()
            for _, row in valid_dividends.iterrows():
                start_date = row['notifyEvent'].date()
                start_idx = np.searchsorted(self.dates_array, start_date)
                
                # --- SỬA LỖI LOGIC: Khớp 100% với logic fallback của bản gốc ---
                exercise_date = row['exerciseDate'].date()
                # if pd.notna(exercise_date) and exercise_date in self.dates_array:
                end_idx = np.searchsorted(self.dates_array, exercise_date, side='right')
                # else:
                    # Logic fallback: index + 10 ngày giao dịch
                    # end_idx = min(start_idx + 10, self.length)
                for i in range(start_idx, end_idx):
                    avoid_set.add(pd.to_datetime(self.dates_array[i]))
            
            if avoid_set:
                avoid_dates_map[stock] = avoid_set

        # Cập nhật box_1
        for date, ranks_by_brand in self.box_1.items():
            if brand in ranks_by_brand:
                ranks_by_brand[brand] = [
                    stock for stock in ranks_by_brand[brand]
                    if ((stock not in avoid_dates_map) or (date not in avoid_dates_map[stock]))
                ]

    def adjust_box(self, not_replace=True, save=False):
        """Điều chỉnh box xếp hạng với các quy tắc."""
        self.box_1 = self.rank_stocks(not_replace=not_replace)
        if not_replace:
            self._replace_score('bank')
            self._replace_score('retail')
            self._avoid_dividend('bank')
            self._avoid_dividend('retail')
            self._avoid_dividend('sec')
            self._avoid_dividend('estate')
        
        if save:
            with open('BOX_1', 'wb') as file:
                pk.dump(self.box_1, file)
        return self.box_1


    def _precompute_conditions(self):
        """
        TÍNH TOÁN TRƯỚC: Gọi hàm JIT-compiled để tăng tốc.
        """
        print("Pre-computing buy conditions with Numba...")
        all_stocks = [s for sublist in self.all_brands.values() for s in sublist]
        self.buy_conditions = {}
        
        for stock in all_stocks:
            stock_np = self.data_numpy.get(stock)
            
            # Gọi hàm đã được JIT-compile với signature đã sửa
            self.buy_conditions[stock] = _loop_precompute(
                stock_np, self.max_time, self.start_idx, self.diff_loc, 
                self.dea_loc
            )
        print("Pre-computing finished.")

    def _precompute_dividend_dates(self):
        """
        Tính toán trước và lưu trữ các ngày thực hiện quyền sắp tới cho mỗi cổ phiếu.
        """
        all_stocks = [s for sublist in self.all_brands.values() for s in sublist]
        self.upcoming_exercise_dates = {}
        for stock in all_stocks:
            dividend_df = stock_dividend.get(stock)
            if dividend_df is not None and not dividend_df.empty:
                valid_dates = np.array(dividend_df['exerciseDate'].dropna().unique())
                if len(valid_dates) > 0:
                    self.upcoming_exercise_dates[stock] = np.sort(valid_dates.ravel().astype('datetime64[D]'))

    # --- HÀM MỚI ---
    def _check_dividend_sell(self, stock, current_date):
        """
        Kiểm tra xem ngày hiện tại có nằm trong vùng bán 3 ngày trước ngày thực hiện quyền không.
        Trả về (True, exercise_date) nếu có, ngược lại (False, None).
        """

        upcoming_dates = self.upcoming_exercise_dates.get(stock)
        if upcoming_dates is None:
            return False, None

        next_event_idx_pos = np.searchsorted(upcoming_dates, current_date)
        if next_event_idx_pos >= len(upcoming_dates):
            return False, None

        next_exercise_date = upcoming_dates[next_event_idx_pos]
        
        current_date_idx = np.searchsorted(self.dates_array, current_date)
        exercise_date_idx = np.searchsorted(self.dates_array, next_exercise_date)
        if exercise_date_idx == len(self.dates_array):
            return False, None
        
        trading_days_until_event = exercise_date_idx - current_date_idx
        
        if 0 < trading_days_until_event <= 3:
            return True, next_exercise_date
            
        return False, None
    def backtest(self, not_replace=True, save=False):
        """Phiên bản backtest được tối ưu hóa và hoàn chỉnh."""
        self.box_1 = self.adjust_box(not_replace=not_replace, save=save)
        self._precompute_conditions()
        self._precompute_dividend_dates()

        # Chuẩn bị các mảng NumPy cho chỉ số ngành
        sec_diffs_a = self.data_numpy['SEC_INDEX'][:, self.df['SEC_INDEX'].columns.get_loc('DIFF_A')]
        sec_oscs_a = self.data_numpy['SEC_INDEX'][:, self.df['SEC_INDEX'].columns.get_loc('OSC_A')]
        sec_index_a = self.data_numpy['SEC_INDEX'][:,13]
        bank_diffs_a = self.data_numpy['BANK_INDEX'][:, self.df['BANK_INDEX'].columns.get_loc('DIFF_A')]
        bank_index_a = self.data_numpy['BANK_INDEX'][:,13]
        retail_diffs_a = self.data_numpy['RETAIL_INDEX'][:, self.df['RETAIL_INDEX'].columns.get_loc('DIFF_A')]
        esta_diffs_a = self.data_numpy['ESTA_CON_INDEX'][:, self.df['ESTA_CON_INDEX'].columns.get_loc('DIFF_A')]
        start_idx = self.start_idx
        # Vòng lặp backtest chính
        for index_day in range(start_idx, self.length):
            day = self.dates_array[index_day]
            list_stocks = self.box_1.get(pdts(day))
            
            # --- Logic chọn số lượng cổ phiếu ---
                # --- TỐI ƯU HÓA: Gọi hàm JIT-compiled ---
            stable_sec_diff, unstable_sec_osc_a, unstable_bank_diff, \
            stable_esta_diff, stable_bank_diff, stable_retail_diff = \
                calculate_industry_conditions_jit(index_day, sec_diffs_a, sec_oscs_a, bank_diffs_a, esta_diffs_a, 
                                                  retail_diffs_a)
            
            if unstable_bank_diff and unstable_sec_osc_a:
                chosen_sec = chosen_retail = chosen_bank = chosen_estate = 0
            elif stable_esta_diff:
                chosen_estate, chosen_bank, chosen_retail, chosen_sec = 2, 1, 1, 1
            elif stable_sec_diff:
                chosen_estate, chosen_sec, chosen_bank, chosen_retail = 1, 2, 1, 1
            elif stable_retail_diff:
                chosen_bank, chosen_sec, chosen_estate, chosen_retail = 1, 1, 1, 2
            elif stable_bank_diff:
                chosen_estate, chosen_sec, chosen_bank, chosen_retail = 1, 1, 2, 1
            else:
                chosen_estate, chosen_sec, chosen_bank, chosen_retail = 1, 2, 1, 1
            
            # --- Logic chọn cổ phiếu cụ thể ---
                # Ưu tiên ngành được lựa chọn nhiều nhất và sau đó là ưu tiên dòng tiền 
            pair = [(chosen_estate,self.data_numpy['ESTA_CON_INDEX'][index_day,-1], 'estate'), 
                    (chosen_sec,self.data_numpy['SEC_INDEX'][index_day,-1], 'sec'), 
                    (chosen_bank,self.data_numpy['BANK_INDEX'][index_day,-1], 'bank'), 
                    (chosen_retail,self.data_numpy['RETAIL_INDEX'][index_day,-1], 'retail')]
            pair_sort = sorted(pair, key=lambda x: [x[0],x[1]], reverse=True)
            
            range_stock_dict = {'estate': [], 'sec': [], 'bank': [], 'retail': []}
            remain_stock = 0
            for count,risk, brand in pair_sort:
                count += remain_stock
                if len(range_stock_dict[brand]) == count:break
                brand_candidates = list_stocks.get(brand, []) if not (mae_jit(sec_index_a[index_day-66:index_day+1],0.013) &
                                          mae_jit(bank_index_a[index_day-66:index_day+1],0.007) & 
                                         (brand in ['estate','sec'])) else []
                for stock in brand_candidates:
                    if self.buy_conditions.get(stock, {})[index_day]:
                        range_stock_dict[brand].append(stock)
                        if len(range_stock_dict[brand]) >= count:break  
                        elif (len(range_stock_dict[brand]) < count) & (stock == brand_candidates[-1]):
                                remain_stock = count - len(range_stock_dict[brand])
            # display(f'{day} HAS {range_stock_dict}')
            final_chosen_stocks = [s for sublist in range_stock_dict.values() for s in sublist]
            final_chosen_stocks.extend([None] * (5 - len(final_chosen_stocks)))
            print(f'{day} có {final_chosen_stocks}')
            store_stock = [
                cs for box, cs in zip(self.box_trades.values(), final_chosen_stocks)
                if box['DATE'][-1] >= day
            ]
            # --- Logic xử lý giao dịch ---
            for box, chosen_stock in zip(self.box_trades.values(), final_chosen_stocks):
                if box['DATE'][-1] >= day: continue
                
                current_stock =chosen_stock
                while True: # Vòng lặp để chọn cổ phiếu thay thế từ store_stock
                    if current_stock is not None:
                        # Thêm dữ liệu cho ngày `day` (ngày ra quyết định)
                        base_cum_profit = box['CUM_PROFIT'][-1]
                        box_data_1 = [day, current_stock, None, 'Good_Sign', None, None, None, None, None, None, None, None, None, 'Good_Sign', base_cum_profit]
                        for k, key in enumerate(box.keys()):box[key].append(box_data_1[k])
                        
                        # Xử lý mua vào ngày hôm sau (index_day + 1)
                        if index_day + 1 >= self.length: break
                        buy_date = self.dates_array[index_day + 1]
                        stock_data_np = self.data_numpy[current_stock]
                        
                        buy_price_open = stock_data_np[index_day + 1, self.open_loc] * 1.0015 * 0.5
                        buy_price_close = stock_data_np[index_day + 1, self.modify_loc] * 1.0015 * 0.5
                        buy_price = buy_price_open + buy_price_close
                        
                        check_date = stock_data_np[index_day + 1, self.ave_loc]
                        profit_init = round(stock_data_np[index_day + 1, self.modify_loc] / buy_price, 4)
                        
                        # Thêm dữ liệu cho ngày mua (index_day + 1)
                        box_data_2 = [buy_date, current_stock, buy_date, buy_price, check_date, None, None, None, None, None, None, None, None, profit_init, round(box['CUM_PROFIT'][-1], 4)]
                        for k, key in enumerate(box.keys()):box[key].append(box_data_2[k])

                        # Không thể bán cổ phiếu sau khi mua tại ngày thứ hai (tức là index_day +2)
                        if index_day + 2 < self.length:
                            index_days_t2 = index_day + 2
                            current_date_t2 = self.dates_array[index_days_t2]
                            check_date_t2 = stock_data_np[index_days_t2, self.modify_loc]
                            profit_t2 = round(check_date_t2 / buy_price, 4)

                            # Thêm dòng dữ liệu cho ngày T+2
                            box['DATE'].append(current_date_t2)
                            box['CHECK_DATE'].append(check_date_t2)
                            box['PROFIT'].append(profit_t2)
                            
                            # Giữ nguyên các giá trị khác từ ngày T+1
                            for key in box.keys():
                                if key not in ['DATE', 'CHECK_DATE', 'PROFIT']:box[key].append(box[key][-1])
                            
                            # Xử lý trường hợp T+2 là ngày cuối cùng của dữ liệu
                            # if self.length - 1 == index_days_t2:
                            #     sell_price = check_date_t2 * (1 - 0.0015)
                            #     profit = round(sell_price / buy_price, 4)
                                
                            #     # Cập nhật lại dòng cuối cùng với thông tin bán
                            #     box['SELL_PRICE'].append(sell_price)
                            #     box['SELL_DATE'].append(current_date_t2)
                            #     box['SELL_DATE_1'].append(current_date_t2)
                            #     box['SELL_DATE_2'].append(current_date_t2)
                            #     box['SELL_PRICE_WEIGHT'].append(sell_price)
                            #     box['PROFIT'].append(profit)
                            #     box['CUM_PROFIT'].append(round(base_cum_profit * profit, 4))
                            #     box['BAD_SIGN'].append('TEMP_END')
                            #     for key in box.keys():
                            #         print(f'{key} HAS {len(box[key])}')

                        i = 3
                        # Vòng lặp nắm giữ cổ phiếu
                        while index_day + i < self.length:
                            index_days = index_day + i
                            current_date = self.dates_array[index_days]
                            # --- TÍCH HỢP LOGIC MỚI ---
                            is_dividend_sell, exercise_date = self._check_dividend_sell(chosen_stock, current_date)
                            
                            if is_dividend_sell:
                                exercise_date_idx = np.searchsorted(self.dates_array, exercise_date)
                                days_to_sell = exercise_date_idx - index_days
                                # print(f'Current: {current_date}, Stock: {chosen_stock}, Exercise: {exercise_date}, Day left: {days_to_sell}, Buy_date: {buy_date}')
                                
                                sell_prices = list(stock_data_np[index_days : index_days + days_to_sell, self.modify_loc])
                                sell_dates = list(self.dates_array[index_days : index_days + days_to_sell])

                                padded_prices = sell_prices + [sell_prices[-1]] * (3 - len(sell_prices))
                                sell_price_weight = np.mean(sell_prices)
                                final_profit = round(sell_price_weight / buy_price, 4)
                                
                                # Đồng bộ hóa
                                for k in range(days_to_sell):
                                    box['DATE'].append(sell_dates[k])
                                    # print(sell_dates[k],chosen_stock)
                                    box['CHECK_DATE'].append(sell_prices[k])
                                    box['PROFIT'].append(round(sell_prices[k]/buy_price, 4))
                                    for key in ['STOCK', 'BUY_DATE', 'BUY_PRICE', 'BAD_SIGN', 'SELL_DATE', 'SELL_DATE_1', 'SELL_DATE_2', 'SELL_PRICE', 'SELL_PRICE_1', 'SELL_PRICE_2', 'SELL_PRICE_WEIGHT']:
                                        box[key].append(box[key][-1])
                                    box['CUM_PROFIT'].append(box['CUM_PROFIT'][-1])
                                
                                box['SELL_PRICE_WEIGHT'][-1] = sell_price_weight
                                box['PROFIT'][-1] = final_profit
                                box['CUM_PROFIT'][-1] = round(box['CUM_PROFIT'][-1] * final_profit, 4)
                                box['BAD_SIGN'][-1] = 'DIVIDEND_SELL'
                                if index_days + days_to_sell <= self.length:
                                    last_cum_profit = box['CUM_PROFIT'][-1]
                                    box['DATE'].append(self.dates_array[index_days + days_to_sell])
                                    # print(f'Ngày bỏ qua khi đã hoàn thành giao dịch tránh cổ tức: {self.dates_array[index_days + days_to_sell]} của {chosen_stock}')
                                    for key in box.keys():
                                        if key not in ['DATE', 'CUM_PROFIT']: box[key].append(None)
                                    box['CUM_PROFIT'].append(round(last_cum_profit, 4))
                                break
                                
                            # --- Logic bán ---
                            # Điều kiện bán

                            osc_1 = (stock_data_np[index_days, self.osc_loc] < -0.005)
                            cut_loss = (stock_data_np[index_days, self.ave_loc]*1/2 + stock_data_np[index_days, 
                                self.modify_loc]*1/2) / buy_price <= 0.955
                            unstable_idx = check_unstable_idx_jit(bank_diffs_a[index_days-5:index_days+1], sec_oscs_a[index_days-7:index_days+1], custom_threshold=-0.0001)
                            # print(f'{self.dates_array[index_days]} HAS {unstable_idx}')
                            days_left = self.length - index_days
                            if days_left < 4:
                                # print(days_left,chosen_stock)
                                # Lấy giá và ngày bán cho những ngày còn lại
                                sell_prices_raw = list(stock_data_np[index_days : index_days + days_left, self.modify_loc])
                                sell_dates_raw = list(self.dates_array[index_days : index_days + days_left])
                            
                                # Lấp đầy danh sách để luôn có 3 phần tử, khớp với logic gốc
                                # Nếu chỉ còn 1 ngày, giá và ngày đó sẽ được lặp lại 3 lần.
                                # Nếu chỉ còn 2 ngày, ngày cuối cùng sẽ được dùng cho vị trí thứ 3.
                                padded_sell_prices = sell_prices_raw +[sell_prices_raw[-1]] * (3 - days_left)
                                padded_sell_dates = sell_dates_raw + [sell_dates_raw[-1]] * (3 - days_left)
                                
                                # Tính toán các giá trị cuối cùng dựa trên dữ liệu đã lấp đầy
                                sell_price, sell_price_1, sell_price_2 = padded_sell_prices
                                sell_date, sell_date_1, sell_date_2 = padded_sell_dates
                                
                                sell_price_weight = np.mean(sell_prices_raw)
                                final_profit = round(sell_price_weight / buy_price, 4)
                                
                                # Xác định bad_sign dựa trên điều kiện bán
                                osc_1 = stock_data_np[index_days, self.osc_loc] < -0.004
                                # print(chosen_stock,stock_data_np[index_days, self.osc_loc])
                                # osc_2 = np.all(np.diff(stock_data_np[index_days - 4 : index_days + 1, self.osc_loc]) < 0)
                                profit_before = (stock_data_np[index_days, self.ave_loc]*2/3+
                                                       stock_data_np[index_days, self.modify_loc]*1/3) / buy_price
                                cut_loss = profit_before < 0.955
                                print(cut_loss)
                                unstable_bank_osc_a = check_unstable_idx_jit(bank_diffs_a[index_days-5:index_days+1], sec_oscs_a[index_days-7:index_days+1], custom_threshold=-0.001)
                                
                                bad_sign = 'REALLY_BAD_SIGN' if (osc_1 or cut_loss or unstable_bank_osc_a) else 'TEMP_END'
                                
                                # --- Đồng bộ hóa tất cả các cột ---
                                # Tính toán khoảng cách chính xác của giao dịch
                                final_sell_date = sell_dates_raw[-1] # Ngày bán thực tế cuối cùng
                                distance = (np.searchsorted(self.dates_array, final_sell_date) - np.searchsorted(self.dates_array, buy_date)) + 1
                                
                                # Số dòng cần thêm vào để hoàn thành giao dịch
                                rows_to_add = distance - (i - 1) # i bắt đầu từ 2, nên i-1 là số ngày đã thêm (T+1)
                            
                                # Thêm các dòng dữ liệu cho những ngày còn lại của giao dịch
                                box['DATE'].extend(self.dates_array[index_days : index_days + rows_to_add])
                                box['STOCK'].extend([current_stock] * rows_to_add)
                                box['BUY_DATE'].extend([buy_date] * rows_to_add)
                                box['BUY_PRICE'].extend([buy_price] * rows_to_add)
                                box['CHECK_DATE'].extend(stock_data_np[index_days : index_days + rows_to_add, self.modify_loc].tolist())
                                
                                profits_to_add = [round(p / buy_price, 4) for p in stock_data_np[index_days : index_days + rows_to_add, self.modify_loc]]
                                box['PROFIT'].extend(profits_to_add)
                                # Điền các thông tin bán vào dòng cuối cùng
                                box['SELL_DATE'].extend([None] * (rows_to_add - 1) + [sell_date])
                                box['SELL_DATE_1'].extend([None] * (rows_to_add - 1) + [sell_date_1])
                                box['SELL_DATE_2'].extend([None] * (rows_to_add - 1) + [sell_date_2])
                                box['SELL_PRICE'].extend([None] * (rows_to_add - 1) + [sell_price])
                                box['SELL_PRICE_1'].extend([None] * (rows_to_add - 1) + [sell_price_1])
                                box['SELL_PRICE_2'].extend([None] * (rows_to_add - 1) + [sell_price_2])
                                box['SELL_PRICE_WEIGHT'].extend([None] * (rows_to_add - 1) + [sell_price_weight])
                                box['BAD_SIGN'].extend([None] * (rows_to_add - 1) + [bad_sign])
                                
                                # Tính toán và điền CUM_PROFIT
                                box['CUM_PROFIT'].extend([base_cum_profit] * (rows_to_add - 1) + [round(base_cum_profit * final_profit, 4)])
                                break
                            elif osc_1 or cut_loss or unstable_idx:
                                # print(self.dates_array[index_days], f'OSC: {osc_1}, CUT_LOSS: {cut_loss}, UNSTABLE INDEX: {unstable_idx}')
                                
                                # Bán trong 3 ngày tới
                                sell_price, sell_price_1, sell_price_2 = stock_data_np[index_days+1:index_days+4, self.modify_loc] * 0.9975
                                sell_price_weight = (sell_price+sell_price_1+sell_price_2)/3
                                profit = round(sell_price_weight / buy_price, 4)
                                
                                sell_date, sell_date_1, sell_date_2 = self.dates_array[index_days+1:index_days+4]

                                # Cập nhật box_trades
                                box['DATE'].extend([self.dates_array[index_days],sell_date, sell_date_1, sell_date_2])
                                for k in ['STOCK', 'BUY_DATE', 'BUY_PRICE']: box[k].extend([box[k][-1]]*4) # Lấy giá trị từ 4 ngày trước
                                box['CHECK_DATE'].extend([stock_data_np[index_days, self.modify_loc],stock_data_np[index_days+1, self.modify_loc], stock_data_np[index_days+2, self.modify_loc], stock_data_np[index_days+3, self.modify_loc]])
                                box['BAD_SIGN'].extend([None]+['SELLING']*3)
                                box['SELL_DATE'].extend([None,sell_date, None, None])
                                box['SELL_DATE_1'].extend([None,None, sell_date_1, None])
                                box['SELL_DATE_2'].extend([None,None, None, sell_date_2])
                                box['SELL_PRICE'].extend([None,sell_price, None, None])
                                box['SELL_PRICE_1'].extend([None,None, sell_price_1, None])
                                box['SELL_PRICE_2'].extend([None,None, None, sell_price_2])
                                box['SELL_PRICE_WEIGHT'].extend([None,None, None, sell_price_weight])
                                box['PROFIT'].extend([round(stock_data_np[index_days, self.modify_loc]/buy_price,4),
                                                      round(sell_price/buy_price, 4), round(sell_price_1/buy_price, 4), profit])
                                box['CUM_PROFIT'].extend([box['CUM_PROFIT'][-1]]*3 + [round(box['CUM_PROFIT'][-1] * profit, 4)]) # Lấy CUM_PROFIT trước khi giao dịch

                                if index_day + i + 4 < self.length:
                                    last_cum_profit = box['CUM_PROFIT'][-1]
                                    box['DATE'].append(self.dates_array[index_day + i + 4])
                                    for key in box.keys():
                                        if key not in ['DATE', 'CUM_PROFIT']: box[key].append(None)
                                    box['CUM_PROFIT'].append(round(last_cum_profit, 4))
                                break # Thoát vòng lặp nắm giữ

                            else: # Nếu không bán, cập nhật trạng thái
                                box['DATE'].append(current_date)
                                for k in ['STOCK', 'BUY_DATE', 'BUY_PRICE', 'BAD_SIGN', 'SELL_DATE', 'SELL_DATE_1','SELL_DATE_2', 'SELL_PRICE', 'SELL_PRICE_1', 'SELL_PRICE_2', 'SELL_PRICE_WEIGHT']: 
                                    box[k].append(box[k][-1])
                                check_date = stock_data_np[index_days, self.modify_loc]
                                box['CHECK_DATE'].append(check_date)
                                profit = round(check_date / buy_price, 4)
                                box['PROFIT'].append(profit)
                                box['CUM_PROFIT'].append(box['CUM_PROFIT'][-1])
                            i += 1
                        break # Thoát vòng lặp while True
                    
                    elif store_stock: # Nếu current_stock là None, lấy từ store_stock
                        # print(f'{day} có danh mục dự phòng là {store_stock}')
                        current_stock = store_stock.pop(0)
#                         print(current_stock)
                        # Lặp lại vòng lặp while True với cổ phiếu mới
                    else: # Không có cổ phiếu nào để mua
                        box_data_none = [day] + [None] * 13 + [round(box['CUM_PROFIT'][-1], 4)]
                        for k, key in enumerate(box.keys()):box[key].append(box_data_none[k])
                        break # Thoát vòng lặp while True

        # --- Tổng hợp kết quả ---
        # for i in range(1,5):
        #     print(i)
        #     try:
        #         dfs = pd.DataFrame(self.box_trades[f'box_trade_{i+1}'])
        #         print(f'{dfs}')
        #     except:
        #         for key in self.box_trades[f'box_trade_{i+1}'].keys():
        #             print(f'{key} HAS {len(self.box_trades[f"box_trade_{i+1}"][key])} LENGTH')
        #         return self.box_trades[f'box_trade_{i+1}'] 
        dfs = [pd.DataFrame(self.box_trades[f'box_trade_{i+1}']) for i in range(5)]
        # Đồng bộ hóa độ dài và explode
        max_len = max(len(d) for d in dfs)
        last_row = dfs[0].iloc[[-1]]
        val_1 = max_len - len(dfs[0])
        name_col = dfs[0].columns
        for i in range(5):
            if len(dfs[i]) < max_len:
                dfs[i] = pd.concat([dfs[i], pd.DataFrame([last_row.values[0]] * val_1 , columns=name_col)], ignore_index=True)
            dfs[i]['DATE'] = dfs[i]['DATE'].explode()

        # Tạo DataFrame tổng hợp
        total_profit = np.dot(
            pd.concat([df['CUM_PROFIT'].fillna(1) for df in dfs], axis=1),
            np.array([0.2]*5)
        )
        
        dfss = pd.DataFrame({
            'DATE': dfs[0]['DATE'].explode(),
            'STOCK_1': dfs[0]['STOCK'], 'STOCK_2': dfs[1]['STOCK'], 'STOCK_3': dfs[2]['STOCK'], 'STOCK_4': dfs[3]['STOCK'], 'STOCK_5': dfs[4]['STOCK'],
            'PROFIT_1': dfs[0]['PROFIT'], 'PROFIT_2': dfs[1]['PROFIT'], 'PROFIT_3': dfs[2]['PROFIT'], 'PROFIT_4': dfs[3]['PROFIT'], 'PROFIT_5': dfs[4]['PROFIT'],
            'TOTAL_PROFIT': total_profit
        })

        return [dfss.reset_index(drop=True)] + [d.reset_index(drop=True) for d in dfs]
In [9]:
with open('BOX_1', 'rb') as file:
    box_2 = pk.load(file)
vnindex = data_1['VNINDEX']

date_begin = datetime.strptime('2025-07-08','%Y-%m-%d').date()
df_adjust = df.copy()
for stock in estate_con_stock:
    df_adjust.drop(columns=[(stock.upper(),'NOTE'), (stock.upper(),'NOTE_AUDIT')],
                                  inplace=True)
manager = OptimizedStockTradeManager(df_adjust,
                                     bank_stock,
                                     retail_stock,
                                     sec_stock,
                                     estate_con_stock,
                                     date_begin=date_begin,
                                     box_2=box_2)
backtest_1 = manager.backtest(not_replace=False,save=False)
backtest_1[0].tail().iloc[:,[0,1,2,3,4,5,-1]]
# pre_compute = manager._precompute_conditions()
Pre-computing buy conditions with Numba...
Pre-computing finished.
2025-07-08 có ['PDR', 'VND', 'VCI', 'VPB', 'DGW']
2025-07-09 có ['PDR', 'VND', 'VCI', 'VPB', 'DGW']
2025-07-10 có ['PDR', 'VND', 'VCI', 'VPB', 'DGW']
2025-07-11 có ['VRE', 'VND', 'VCI', 'VPB', 'MWG']
2025-07-14 có ['VRE', 'VCI', 'SSI', 'VPB', 'MWG']
2025-07-15 có ['VRE', 'PDR', 'VND', 'VPB', 'MWG']
2025-07-16 có ['VRE', 'PDR', 'VND', 'CTG', 'FRT']
2025-07-17 có ['VRE', 'PDR', 'VND', 'VPB', 'MWG']
2025-07-18 có ['VRE', 'PDR', 'VND', 'VPB', 'MWG']
2025-07-21 có ['KBC', 'VCG', 'VIX', 'VPB', 'FRT']
2025-07-22 có ['VRE', 'PDR', 'VND', 'VPB', 'MWG']
2025-07-23 có ['VRE', 'PDR', 'VND', 'VPB', 'DGW']
2025-07-24 có ['PDR', 'NVL', 'VND', 'VPB', 'DGW']
2025-07-25 có ['VRE', 'PDR', 'VND', 'VPB', 'DGW']
2025-07-28 có ['VRE', 'PDR', 'VND', 'VPB', 'DGW']
2025-07-29 có ['PDR', 'VND', 'MBS', 'VPB', None]
2025-07-30 có ['VGC', 'MBS', 'SHS', 'CTG', None]
2025-07-31 có ['PDR', 'VND', 'VCI', 'VPB', None]
2025-08-01 có ['PDR', 'VCI', 'VIX', 'EIB', None]
2025-08-04 có ['NVL', 'VND', 'VCI', 'VPB', None]
2025-08-05 có ['NVL', 'VND', 'VCI', 'HDB', None]
2025-08-06 có ['PDR', 'VND', 'VCI', 'TPB', None]
2025-08-07 có ['NVL', 'VND', 'VCI', 'HDB', None]
2025-08-08 có ['PDR', 'VND', 'VCI', 'HDB', 'MWG']
2025-08-11 có ['KDH', 'VND', 'VCI', 'MBB', 'MWG']
2025-08-12 có ['KDH', 'VND', 'VCI', 'MBB', 'MWG']
2025-08-13 có ['KDH', 'HCM', 'MBB', 'SHB', None]
2025-08-14 có ['KDH', 'HCM', 'TPB', 'MBB', None]
2025-08-15 có ['PDR', 'HCM', 'MBB', 'SHB', None]
2025-08-18 có ['KDH', 'VIX', 'TPB', 'SHB', None]
2025-08-19 có ['KDH', 'HCM', 'MBB', 'TPB', None]
2025-08-20 có ['KBC', 'TPB', 'VPB', None, None]
2025-08-21 có ['KDH', 'MBS', 'MBB', 'TPB', None]
2025-08-22 có ['VIB', None, None, None, None]
2025-08-25 có ['PDR', 'VIX', None, None, None]
2025-08-26 có ['PDR', 'ACB', None, None, None]
2025-08-27 có ['KDH', 'VIX', 'ACB', None, None]
2025-08-28 có ['VIC', 'VHM', 'VIX', 'HDB', None]
2025-08-29 có ['KDH', 'PDR', 'VIX', 'HDB', 'MWG']
2025-09-03 có ['KDH', 'PDR', 'VND', 'HDB', None]
2025-09-04 có ['PDR', 'VND', 'VCI', 'HDB', None]
2025-09-05 có ['PDR', 'VCI', 'HCM', 'HDB', 'MWG']
2025-09-08 có [None, None, None, None, None]
2025-09-09 có ['DXG', 'SSI', 'MWG', None, None]
2025-09-10 có [None, None, None, None, None]
2025-09-11 có [None, None, None, None, None]
2025-09-12 có [None, None, None, None, None]
2025-09-15 có [None, None, None, None, None]
2025-09-16 có ['CTD', 'SSI', 'MWG', 'FRT', None]
False
2025-09-17 có ['CTD', 'FRT', None, None, None]
False
2025-09-18 có ['VCG', None, None, None, None]
2025-09-19 có ['CTD', 'FRT', None, None, None]
False
2025-09-22 có [None, None, None, None, None]
2025-09-23 có [None, None, None, None, None]
2025-09-24 có [None, None, None, None, None]
2025-09-25 có ['CTD', 'FRT', None, None, None]
2025-09-26 có ['CTD', 'FRT', None, None, None]
True
2025-09-29 có ['CTD', 'FRT', None, None, None]
False
2025-09-30 có ['CTD', 'FRT', None, None, None]
2025-10-01 có ['FRT', None, None, None, None]
2025-10-02 có ['VIC', 'CTR', 'STB', None, None]
2025-10-03 có ['VRE', 'VIC', None, None, None]
2025-10-06 có ['CTD', 'VRE', 'STB', None, None]
2025-10-07 có ['CTD', 'VRE', 'STB', 'FRT', None]
Out[9]:
DATE STOCK_1 STOCK_2 STOCK_3 STOCK_4 STOCK_5 TOTAL_PROFIT
59 2025-10-01 CTD FRT CTD CTD CTD 1.1933
60 2025-10-02 CTD FRT CTD CTD CTD 1.1933
61 2025-10-03 CTD FRT CTD CTD CTD 1.1933
62 2025-10-06 CTD FRT CTD CTD CTD 1.1933
63 2025-10-07 CTD FRT CTD CTD CTD 1.2121
In [10]:
rrd_1=rrd()
rrd_1
'DATA OF steel DO NOT HAVE normal family distribution with p_value 0.0143'
'DATA OF fishery DO NOT HAVE normal family distribution with p_value 0.0998'
Out[10]:
shape: (4, 15)
criteriabanksecreal_estateconstrucrubberoilretailfertilizertransportsteelmanufacturergarmentfisheryother
strf64f64f64f64f64f64f64f64f64f64f64f64f64f64
"Median"0.24390.15570.09840.03710.00590.01170.02640.01710.02570.0530.03520.00310.00580.037
"Std"0.04430.04380.02690.01170.0040.00920.00930.00870.00870.0530.01210.0020.00630.0144
"Mean 5-period"0.27530.17940.11720.04310.00260.0070.02710.01360.02140.06460.03330.00190.00990.0401
"Nearest Value"0.25410.22630.11360.05710.00190.00630.03080.01040.01970.060.03190.00160.00720.0353
In [11]:
result_bs = df_bs()
result_bs.head(5)
Out[11]:
BANK SEC REAL_ESTATE CONSTRUC RUBBER OIL RETAIL FERTILIZER TRANSPORT STEEL MANUFACTURER GARMENT FISHERY OTHER
2025-10-07 -706,676.6175 -176,855.7975 -110,753.8300 -120,985.4238 -2,622.0687 -33,504.8475 54,373.5437 -55,822.8613 -109,533.4337 -232,506.3238 -75,336.9475 -6,443.8712 -50,801.2582 -8,857.0400
2025-10-06 2,927,120.8350 2,035,355.1575 507,343.5737 181,007.1325 11,841.9225 30,263.5638 32,351.7462 65,104.5163 68,906.8337 818,324.8488 280,707.1512 11,039.5612 9,620.0915 207,258.7500
2025-10-03 -658,566.3100 -654,445.5463 -158,366.3600 -54,181.6413 -37,510.3438 -77,872.4362 40,671.3363 -79,625.1687 -61,401.3125 -506,861.0587 -203,943.0988 -6,484.2688 85,257.8887 148,519.2025
2025-10-02 -221,328.6375 -971,975.6275 -777,703.6012 -303,073.6763 -26,712.8700 -37,972.6988 -96,963.6362 -165,916.9138 -187,155.8450 -178,112.6350 -110,297.0975 6,982.6363 -48,095.7865 -162,101.2725
2025-10-01 880,904.0413 387,496.6050 -263,010.4738 -40,281.0700 6,577.5700 -40,294.5137 -8,325.6388 -60,946.6125 -53,687.9300 -116,031.2800 882.4600 2,382.2700 4,514.4460 -97,364.6125

Realized_Volatility¶

In [ ]:
stock = "FTS"

# --- daily data (pandas) ---
stock_chosen = (
    data_1[stock]
    .loc[lambda df: df["DATE"] >= pd.to_datetime("2019-01-21")]
    .sort_values("DATE", ignore_index=True)
)

stock_chosen_ratio = stock_chosen.iloc[:, 2] / stock_chosen.iloc[:, 1]

# stock_chosen_ratio= stock_chosen_ratio.iloc[:-1].reset_index(drop=True)
# stock_chosen_ratio=pd.concat([pd.Series([stock_chosen_ratio[0]],dtype=stock_chosen_ratio.dtype),stock_chosen_ratio],ignore_index=True)
# stock_chosen_ratio.reset_index(drop=True,inplace=True)
# stock_chosen_ratio.loc[len(stock_chosen_ratio)]=stock

stock_chosen['RATIO'] = stock_chosen_ratio

# Convert sang polars (chỉ giữ cột cần thiết)
daily_pl = pl.from_pandas(stock_chosen[["DATE", "RATIO"]])
daily_pl = daily_pl.with_columns(pl.col("DATE").dt.date().alias("Date"))  # chuẩn hóa format

# --- 1m data (parquet) ---
stock_1m_raw = pl.read_parquet(
    f"C:/Users/duytk/data_1m/{stock}_1m.parquet", use_statistics=True
).filter(pl.col("Date") != datetime(2023, 8, 20).date())

# --- join trực tiếp ---
stock_1m = stock_1m_raw.join(daily_pl, on="Date", how="left")

# rename cho gọn
stock_1m = stock_1m.rename({"RATIO": "Ratio"}).drop("DATE")
stock_1m = stock_1m.with_columns((pl.col("Ave").truediv(pl.col('Ratio'))).alias('Ave_Adj'))

result_df = stock_1m.with_columns(pl.col('Ave_Adj').log().diff().alias("log_return")).group_by(
        ['Symbol',"Date"]).agg(
        # Tính tổng 'Value' cho các dòng có 'Match_Type' là 'Buy Up'
        pl.col("Value").filter(pl.col("Match_Type") == "Buy Up").sum().fill_null(0).alias("Sum_BuyUp"),
    
        # Tính tổng 'Value' cho các dòng có 'Match_Type' là 'Sell Down'
        pl.col("Value").filter(pl.col("Match_Type") == "Sell Down").sum().fill_null(0).alias("Sum_SellDown"),
    
        # Tính tổng 'Volume' cho cả ngày
        pl.col("Volume").sum().alias("Total_Volume"),
        pl.col("log_return").pow(2).sum().sqrt().alias("Realized_Volatility")
        ).sort('Date')
result_final = result_df.with_columns(((pl.col("Sum_BuyUp") - pl.col("Sum_SellDown")).rolling_sum(window_size=25)/pl.col('Total_Volume').rolling_sum(window_size=25)).alias('INFI_ADJ'))
In [ ]:
px.line(result_final,y='Realized_Volatility',x='Date')
In [ ]:
stock_1m.filter(pl.col('Date')==datetime.strptime('2023-05-10','%Y-%m-%d').date())

R PROCESS¶

In [ ]:
vnindex_1m_raw = pl.read_parquet(r"C:\Users\duytk\data_1m\VNINDEX_1m.parquet")
list_price = ['Close','High','Low']
for price in list_price: 
    vnindex_1m_raw=vnindex_1m_raw.with_columns(pl.when(pl.col(price)<10).then(pl.col(price)*1000).otherwise(pl.col(price)))
    vnindex_1m_raw=vnindex_1m_raw.with_columns(pl.when(pl.col(price)<200).then(pl.col(price)*10).otherwise(pl.col(price)))
vnindex_1m_raw=vnindex_1m_raw.filter(pl.col('Date')!=datetime.strptime('2023-08-20','%Y-%m-%d'))
vnindex_1m=vnindex_1m_raw.with_columns(
    pl.col("Close").log().diff().alias("log_return")
).group_by("Date").agg(
    pl.col("log_return").pow(2).sum().alias("Realized_Volatility")
).sort("Date")

vnindex_adjust = vnindex[vnindex.loc[:,'DATE']>=pdts('2019-01-21')].sort_values('DATE',ignore_index=True)
vnindex_adjust['Return_Vnindex'] = np.log(vnindex_adjust['MODIFY_PRICE']).diff()

np_date = vnindex_adjust.loc[:,'DATE'].values
return_vnindex = []
for date_adjust in vnindex_1m['Date']:
    loc = np.where(np_date==pdts(date_adjust))[0][0]
    return_vnindex.append(vnindex_adjust.iat[loc,-1]) 
vnindex_1m_pandas=vnindex_1m.to_pandas()
vnindex_1m_pandas.insert(loc=2,column='Return_Vnindex',value=return_vnindex)
vnindex_1m_pandas=vnindex_1m_pandas.dropna(axis=0,ignore_index=True)
In [ ]:
%%R -i vnindex_1m_pandas
library('rugarch')
library('stats')
library('dplyr')
library('WeightedPortTest')
library('xts')
library('rmgarch')
# options(device = "png")
# png("NUL") 
# dev.off()
In [ ]:
%%R
# Tải các thư viện cần thiết

# --- BƯỚC 1: CHUẨN BỊ DỮ LIỆU MỘT CÁCH CHẶT CHẼ ---

# Chuyển đổi cột Date từ Python sang đúng định dạng Date của R
vnindex_1m_pandas$Date <- as.Date(vnindex_1m_pandas$Date)

# Tạo một đối tượng xts duy nhất chứa TẤT CẢ dữ liệu cần thiết.
# Điều này đảm bảo sự đồng bộ tuyệt đối giữa ngày tháng và các chuỗi dữ liệu.
data_xts <- xts(vnindex_1m_pandas[, c("Return_Vnindex", "Realized_Volatility")], 
                order.by = vnindex_1m_pandas$Date)

# Loại bỏ tất cả các hàng có chứa bất kỳ giá trị NA nào TRÊN ĐỐI TƯỢNG XTS
data_xts_clean <- na.omit(data_xts)


# --- BƯỚC 2: CẮT DỮ LIỆU ĐỂ FIT MÔ HÌNH ---

date_string <- '2025-09-26'

# Cắt dữ liệu huấn luyện từ đầu đến ngày đã chọn
# Cú pháp " /YYYY-MM-DD" của xts rất mạnh mẽ và an toàn
training_data <- data_xts_clean[paste0("/", date_string)]

# Bây giờ, trích xuất các chuỗi dữ liệu đã được làm sạch và đồng bộ
return_ave <- training_data$Return_Vnindex
real_vol_clean <- training_data$Realized_Volatility


# --- BƯỚC 3: FIT MÔ HÌNH ---

garch_model <- 'realGARCH'
ar_order <- 1
ma_order <- 1
dis <- 'ghyp'
ctrl <- list(rho = 1, delta = 1e-8, outer.iter = 100, inner.iter = 650, tol = 1e-8)

# start.pars không cần thiết khi dùng solver mặc định, nhưng vẫn giữ lại nếu anh/chị muốn tùy chỉnh
garch_spec <- ugarchspec(variance.model=list(model=garch_model),
                         mean.model=list(armaOrder=c(ar_order,ma_order)),
                         distribution.model = dis)

# Fit mô hình với dữ liệu đã được chuẩn bị hoàn hảo
# Không cần dùng as.xts() nữa vì real_vol_clean đã là một đối tượng xts
garch_fit <- try(ugarchfit(data = return_ave , 
                           spec = garch_spec, 
                           solver = "hybrid", 
                           solver.control = ctrl,
                           realizedVol = real_vol_clean
                           ))

# In kết quả để kiểm tra
show(garch_fit)
# cl <- makeCluster(12)
 
# garch_roll<-ugarchroll(garch_spec,return_ave,forecast.length=100,refit.every=1,refit.window="recursive",calculate.VaR = TRUE, VaR.alpha = c(0.01,0.05),cluster=cl,,realizedVol = real_vol_clean)
# stopCluster(cl)
# show(garch_roll)
In [ ]:
%%R
# --- BƯỚC 1: TRÍCH XUẤT CÁC THÀNH PHẦN MỘT CÁCH CHÍNH XÁC ---

# 1a. Lấy vector chứa tất cả các tham số đã được ước lượng
all_coeffs <- coef(garch_fit)

# 1b. Trích xuất từng tham số của Phương trình Đo lường bằng TÊN CHÍNH XÁC
xi_param <- all_coeffs["xi"]
# Giả định phi = 1 vì nó không có trong kết quả
phi_param <- 1 
delta_param <- all_coeffs["delta"]
lambda_param <- all_coeffs["lambda"]


# 1c. Lấy chuỗi phương sai có điều kiện (σ_t^2)
conditional_variance <- sigma(garch_fit)^2

# 1d. Lấy chuỗi phần dư chuẩn hóa (z_t)
std_residuals <- residuals(garch_fit, standardize = FALSE)


# --- BƯỚC 2: TÁI CẤU TRÚC LẠI GIÁ TRỊ FIT ---

# 2a. Tái cấu trúc lại thành phần đòn bẩy delta(z_t) với đúng tên tham số
leverage_component_delta <- delta_param * std_residuals + lambda_param * (std_residuals^2 - 1)

# 2b. Tính toán logarit của giá trị "fit" theo đúng công thức
fitted_realized_log <- xi_param + phi_param * log(conditional_variance) + leverage_component_delta

# 2c. Chuyển đổi trở lại thang đo thông thường bằng hàm mũ
fitted_realized_values <- exp(fitted_realized_log)


# --- BƯỚC 3: KIỂM TRA VÀ SỬ DỤNG ---

# Lấy lại chuỗi Biến động Thực đã được sử dụng để so sánh
realized_vol_used <- real_vol_clean 

# print("Giá trị Thực tế (x_t) vs. Giá trị Fit được tái cấu trúc (E[x_t])")
print(tail(cbind(realized_vol_used, fitted_realized_values)))

# Trực quan hóa để so sánh
# plot(realized_vol_used, main = "Biến động Thực Tế vs. Giá trị Fit (Đã sửa lỗi)", col = "grey")
# lines(fitted_realized_values, col = "blue")
# legend("topright", legend=c("Thực Tế", "Fit"), col=c("grey", "blue"), lty=1)
In [ ]:
fitted_values = %R fitted(garch_fit)
sigma_garch = %R fitted_realized_values

vnindex_1m_pandas.insert(2,'Sigma_Garch',np.concatenate(sigma_garch).ravel())

vnindex_1m_pandas['Unexpected_Vol'] = vnindex_1m_pandas['Sigma_Garch'] - vnindex_1m_pandas['Realized_Volatility']
vnindex_1m_pandas = vnindex_1m_pandas[vnindex_1m_pandas.loc[:,'Date']>=datetime.strptime('2021-07-07','%Y-%m-%d')].reset_index(drop=True)
In [ ]:
# --- Bắt đầu tính toán ---

# Bước 1: Tính toán các thành phần của CMF cho mỗi phút
df_with_mf = vnindex_1m_raw.with_columns(
    # 1. Money Flow Multiplier (Hệ số Nhân Dòng tiền)
    # Xử lý trường hợp High == Low để tránh chia cho 0
    pl.when(pl.col("High") != pl.col("Low"))
    .then(
        ((pl.col("Close") - pl.col("Low")) - (pl.col("High") - pl.col("Close"))) / 
        (pl.col("High") - pl.col("Low"))
    )
    .otherwise(0) # Nếu High == Low, hệ số là 0
    .alias("money_flow_multiplier"),
).with_columns(
    # 2. Money Flow Volume (Khối lượng Dòng tiền)
    (pl.col("money_flow_multiplier") * pl.col("Volume")).alias("money_flow_volume")
)

# Bước 2: Tổng hợp CMF theo ngày để tạo ra một chỉ số đại diện
daily_cmf = df_with_mf.group_by("Date").agg(
    # Tính tổng ròng của dòng tiền và tổng khối lượng trong ngày
    pl.col("money_flow_volume").sum().alias("net_daily_mfv"),
    pl.col("Volume").sum().alias("total_daily_volume")
).with_columns(
    # Chỉ số CMF đại diện cho ngày = Tổng ròng Dòng tiền / Tổng Khối lượng
    (pl.col("net_daily_mfv") / pl.col("total_daily_volume")).alias("daily_cmf_indicator")
).sort("Date")


daily_cmf=daily_cmf.filter(pl.col('Date')>=datetime.strptime('2021-07-07','%Y-%m-%d').date())
# # (Tùy chọn) Chuẩn hóa chỉ số CMF hàng ngày bằng Z-score để tạo Trục Y
# # Giả sử chúng ta có đủ dữ liệu để tính Z-score trên cửa sổ 50 ngày
rolling_window = 25 # Thay đổi giá trị ở đây
daily_cmf_zscore = daily_cmf.with_columns(
    (pl.col("daily_cmf_indicator").rolling_mean(rolling_window)).alias("rolling_cmf_25")
)
# print("\n----- Trục Y: Chỉ số CMF đã được chuẩn hóa (Z-score) -----")
# display(daily_cmf_zscore)
cmf_rolling_pandas = daily_cmf_zscore.to_pandas()
vnindex_1m_pandas['CMF_Ind'] = cmf_rolling_pandas['daily_cmf_indicator']
vnindex_1m_pandas['CMF_Rolling'] = cmf_rolling_pandas['rolling_cmf_25']

vnindex_adjust = vnindex[vnindex.loc[:,'DATE']>=pdts('2021-07-07')].sort_values('DATE',ignore_index=True)
np_date = vnindex_adjust['DATE'].values
values_vnindex=[]
for date_adjust in vnindex_1m_pandas['Date']:
    loc = np.where(np_date == date_adjust)[0][0]
    values_vnindex.append(vnindex_adjust.iat[loc,5])
series_values = pd.Series(values_vnindex).pct_change()
series_rolling = series_values.rolling(window=25).apply(lambda x: (1 + x).prod() - 1)

vnindex_1m_pandas.insert(loc=6,column='Values_Vnindex',value=series_rolling)
# vnindex_1m_pandas['Date'] = vnindex_1m_pandas['Date'].dt.strftime("%Y-%m-%d")
vnindex_1m_pandas['Date'] = pd.to_datetime(vnindex_1m_pandas['Date'])
In [ ]:
# --- TẢI VÀ XỬ LÝ DỮ LIỆU ---
# file_path = 'vnindex_1m_pandas.csv'
# try:
#     df = pd.read_csv(file_path)
# except FileNotFoundError:
#     print(f"Lỗi: Không tìm thấy tệp tại đường dẫn: {file_path}")
#     exit()
from plotly.offline import plot

vnindex_1m_pandas.dropna(subset=['Unexpected_Vol', 'CMF_Ind', 'Values_Vnindex','CMF_Rolling'], inplace=True)
vnindex_1m_pandas.sort_values('Date', inplace=True)
vnindex_1m_pandas['Abs_Unexpected_Vol'] = abs(vnindex_1m_pandas['Unexpected_Vol'])

# Tìm giá trị tuyệt đối lớn nhất cho mỗi chuỗi dữ liệu
cmf_range = max(abs(vnindex_1m_pandas['CMF_Rolling'].min()), abs(vnindex_1m_pandas['CMF_Rolling'].max()))*1.1
return_range = max(abs(vnindex_1m_pandas['Values_Vnindex'].min()), abs(vnindex_1m_pandas['Values_Vnindex'].max())) * 1.1 # Thêm 10% đệm

# --- TÍNH TOÁN CÁC CHỈ BÁO MỚI ---
# Tính lợi nhuận (%) của Giá trị Giao dịch
# df['Value_Return'] = df['Values_Vnindex'].pct_change() * 100
# Tạo màu sắc cho biểu đồ cột
bar_colors = np.where(vnindex_1m_pandas['Values_Vnindex'] >= 0, '#1DD1A1', '#FF6B81')
scatter_colors = np.where(vnindex_1m_pandas['Return_Vnindex'] >= 0, '#1DD1A1', '#FF6B81')

vnindex_1m_pandas.dropna(inplace=True) # Loại bỏ hàng đầu tiên có NaN do pct_change()

# --- VẼ BIỂU ĐỒ KẾT HỢP VỚI BỐ CỤC MỚI ---
# Tạo một figure với lưới 2x2 và trục Y thứ hai cho biểu đồ ở vị trí 2,1
fig = make_subplots(
    rows=2, cols=2,
    column_widths=[0.5, 0.5],     # Cột 1 hẹp hơn, Cột 2 rộng hơn
    row_heights=[0.2, 0.8],       # Hàng 1 thấp hơn, Hàng 2 cao hơn
    specs=[[{}, {"rowspan": 2}],
           [{"secondary_y": True}, None]], # Thêm trục Y thứ hai
    subplot_titles=(
        "1. Biến Động Bất Ngờ Theo Thời Gian",
        "3. Biểu Đồ Quỹ Đạo Trạng Thái 2D",
        "2. Dòng Tiền (Line) vs. Động Lượng Giá Trị (Bar)"
    ),
    vertical_spacing=0.07, # Giảm khoảng trống dọc
    horizontal_spacing = 0.08
)

# --- Biểu đồ 1: Unexpected_Vol vs. Time (Vị trí 1,1) ---
fig.add_trace(
    go.Scatter(x=vnindex_1m_pandas['Date'], y=vnindex_1m_pandas['Unexpected_Vol'], mode='lines', name='Unexpected Vol',
               line=dict(color='cyan', width=1.5)),
    row=1, col=1
)

# --- Biểu đồ 2: CMF (line) và Value_Return (bar) (Vị trí 2,1) ---
# Thêm biểu đồ đường cho CMF trên trục Y chính
fig.add_trace(
    go.Scatter(
        x=vnindex_1m_pandas['Date'],
        y=vnindex_1m_pandas['CMF_Rolling'], # Sử dụng CMF_Ind thay vì CMF_Rolling để có tín hiệu nhạy hơn
        mode='lines',
        name='CMF Indicator',
        line=dict(color='#0052CC', width=1)
    ),
    row=2, col=1, secondary_y=False
)
# Thêm biểu đồ cột cho Lợi nhuận Giá trị trên trục Y phụ
fig.add_trace(
    go.Bar(
        x=vnindex_1m_pandas['Date'],
        y=vnindex_1m_pandas['Values_Vnindex'],
        name='Lợi nhuận GTGD (%)',
        marker_color=bar_colors,
        opacity = 1,
    ),
    row=2, col=1, secondary_y=True
)

# --- Biểu đồ 3: Quỹ đạo 2D (Vị trí 1,2 và 2,2) ---
fig_path = px.scatter(
    vnindex_1m_pandas.iloc[-100:,:], x="Unexpected_Vol", y="CMF_Ind", size="Abs_Unexpected_Vol",
    size_max=30, hover_name="Date", color = 'Return_Vnindex' , color_discrete_sequence= scatter_colors
)
for trace in fig_path.data:
    fig.add_trace(trace, row=1, col=2)
fig.add_vline(x=0, line_width=1, line_dash="dash", line_color="grey", row=1, col=2)
fig.add_hline(y=0, line_width=1, line_dash="dash", line_color="grey", row=1, col=2)


# --- CẬP NHẬT LAYOUT TỔNG THỂ ---
fig.update_layout(
    height=800,
    width=1700,
    title_text="Bảng Điều Khiển Phân Tích Động Lực Học Thị Trường VNINDEX",
    template="plotly_dark",
    showlegend=False,
    bargap=0.0, # Giảm khoảng cách giữa các cột

)
# Cập nhật các trục Y cho biểu đồ thứ hai để đồng bộ mốc 0
fig.update_yaxes(title_text="Sức Mạnh Dòng Tiền (CMF)", row=2, col=1, secondary_y=False,zeroline=True, 
                 range=[-cmf_range, cmf_range],showgrid=False) # Set thang đo đối xứng
fig.update_yaxes(title_text="Lợi nhuận GTGD (%)", row=2, col=1, secondary_y=True,zeroline = True,
                 range=[-return_range,return_range],showgrid=False) # Set thang đo đối xứng

# Cập nhật các trục còn lại
fig.update_xaxes(title_text="Thời Gian", row=1, col=2, rangebreaks=[dict(bounds=["sat", "sun"])])
fig.update_yaxes(title_text="Biến Động Bất Ngờ", row=1, col=1)
fig.update_xaxes(title_text="Đà Tăng Cảm Xúc", row=1, col=2)
fig.update_yaxes(title_text="Sức Mạnh Dòng Tiền", row=1, col=2)

# Lưu biểu đồ
# fig.write_html("vnindex_2d_dashboard_dual_axis.html")

print("Bảng phân tích đã được cập nhật với biểu đồ 2 trục Y và lưu vào tệp 'vnindex_2d_dashboard_dual_axis.html'.")

INF PLOT¶

In [14]:
brand_infi=pd.concat([df.loc[:,'DATE']['DATE'],
           df.loc[:,'BANK_INDEX']['BANK_INFI'],
           df.loc[:,'ESTA_CON_INDEX']['ESTA_CON_INFI'],
           df.loc[:,'SEC_INDEX']['SEC_INFI'],
           df.loc[:,'RETAIL_INDEX']['RETAIL_INFI']
          
          ],axis=1)

# --- Các tham số chính ---
window_size = 25
degrees_of_freedom = window_size - 1

# --- Tính toán hằng số tương đương với phân phối t-student ---
# Hằng số này là nghịch đảo của phân vị thứ 75 (0.75 quantile) của phân phối t-student
# với bậc tự do = 24. Nó sẽ lớn hơn 1.4826, phản ánh tính chất "đuôi lệch"
t_scale_factor = 1 / t.ppf(0.9, df=degrees_of_freedom)
print(f"Hằng số tương đương với phân phối t-student (df={degrees_of_freedom}): {t_scale_factor:.4f}")

# --- Tính Robust Z-Score cho tất cả các cột INFI ---
for column in brand_infi.columns[1:]:
    # Bước 1: Tính trung vị trượt
    rolling_median = brand_infi[column].rolling(window=window_size).median()
    
    # Bước 2: Tính Median Absolute Deviation (MAD) trượt
    def mad_func(x):
        return np.median(np.abs((x - np.median(x))))

    median_abs_dev = brand_infi[column].rolling(window=window_size).apply(mad_func, raw=True)
    
    # Bước 3: Tính Robust Z-Score với hằng số t-student
    brand_infi[f'{column}_ZSCORE_T'] = (brand_infi[column] - rolling_median) / (t_scale_factor * median_abs_dev)

# In ra kết quả để xem
print("\n--- DataFrame Sau Khi Thêm Cột Robust Z-Score ---")
brand_infi_zscore = brand_infi.iloc[:,[0,5,6,7,8]]

cif_col = ['BANK_CIF','ESTA_CIF','SEC_CIF','RETAIL_CIF']
for new_col,old_col in zip(cif_col,brand_infi_zscore.columns[1:]):    
    # Sử dụng .rolling().sum() để tính tổng trượt
    brand_infi[new_col] = brand_infi[old_col].rolling(window=window_size).sum()

brand_cif = brand_infi.iloc[:,[0,-4,-3,-2,-1]]
Hằng số tương đương với phân phối t-student (df=24): 0.7588

--- DataFrame Sau Khi Thêm Cột Robust Z-Score ---
In [15]:
px.line(y=df.loc[:,['DATE','CTD']].iloc[:,-1].values,x=df['DATE']['DATE'].values)
In [16]:
px.line(y=['BANK_CIF','SEC_CIF','ESTA_CIF','RETAIL_CIF'],x='DATE',data_frame=brand_cif)
In [23]:
def rolling_cumulative_return_np(stock, window_size):
    """
    Tính toán lợi nhuận tích lũy trên cửa sổ trượt bằng NumPy.

    Args:
        daily_returns (np.ndarray): Mảng 1D chứa tỷ suất lợi nhuận hàng ngày.
        window_size (int): Kích thước của cửa sổ trượt (ví dụ: 25).

    Returns:
        np.ndarray: Mảng 1D chứa lợi nhuận tích lũy trên từng cửa sổ.
                    Các giá trị đầu tiên sẽ là NaN.
    """
    # Bước 0: Lựa chọn giá trị để tính lợi nhuận
    daily_returns = np.diff(np.log(df.loc[:,stock]['AVE_PRICE'].values[1:]))
    
    # Bước 1: Chuyển đổi lợi nhuận hàng ngày thành hệ số tăng trưởng (1 + r)
    growth_factors = daily_returns + 1
    
    # Bước 2: Sử dụng as_strided để tạo một "view" 2D của các cửa sổ trượt
    # mà không cần sao chép dữ liệu, giúp tiết kiệm bộ nhớ và tăng tốc độ.
    shape = (growth_factors.shape[0] - window_size + 1, window_size)
    strides = (growth_factors.strides[0], growth_factors.strides[0])
    rolling_windows = np.lib.stride_tricks.as_strided(
        growth_factors, shape=shape, strides=strides
    )
    
    # Bước 3: Tính tích lũy (cumulative product) trên mỗi cửa sổ và trừ đi 1
    # np.prod(axis=1) sẽ tính tích của các phần tử trên mỗi hàng (mỗi cửa sổ)
    cumulative_returns = np.prod(rolling_windows, axis=1) - 1
    
    # Bước 4: Tạo một mảng kết quả với đệm NaN ở đầu
    # vì không thể tính cho các phiên đầu tiên
    result = np.full(daily_returns.shape, np.nan)
    result[window_size - 1:] = cumulative_returns
    
    return result

def cumulative_returns_vs_inf(stock):
    # --- BƯỚC 2: VẼ BIỂU ĐỒ KẾT HỢP VỚI 2 TRỤC Y ---
    dates = df.loc[:,'DATE']['DATE'].values[1:]
    rolling_returns = rolling_cumulative_return_np(stock,25)
    money_flow_data = df.loc[:,stock_fig].iloc[1:,-1]
    # Khởi tạo figure với secondary_y=True để có 2 trục Y
    fig = make_subplots(specs=[[{"secondary_y": True}]])
    
    # Lấy giá trị min/max thực tế của dữ liệu, bỏ qua các giá trị NaN
    y1_min, y1_max = np.nanmin(rolling_returns), np.nanmax(rolling_returns)
    y2_min, y2_max = np.nanmin(money_flow_data), np.nanmax(money_flow_data)
    
    # Tính toán tỷ lệ giữa phần dương và phần âm cho mỗi trục
    # Thêm xử lý trường hợp một trong hai min/max bằng 0 để tránh lỗi chia cho 0
    ratio1 = y1_max / abs(y1_min) if y1_min != 0 else np.inf
    ratio2 = y2_max / abs(y2_min) if y2_min != 0 else np.inf
    
    # Tìm ra tỷ lệ "chênh lệch" lớn nhất
    master_ratio = max(ratio1, ratio2)
    
    # Dựa trên master_ratio, xác định lại phạm vi mới cho cả hai trục
    # Điều này đảm bảo trục nào "ít chênh lệch" hơn sẽ được mở rộng để khớp với trục kia
    new_y1_max = max(y1_max, abs(y1_min) * master_ratio) if y1_min < 0 else y1_max
    new_y1_min = min(y1_min, -y1_max / master_ratio) if y1_max > 0 else y1_min
    
    new_y2_max = max(y2_max, abs(y2_min) * master_ratio) if y2_min < 0 else y2_max
    new_y2_min = min(y2_min, -y2_max / master_ratio) if y2_max > 0 else y2_min
    
    # Thêm một chút đệm (padding) 5% để đồ thị không bị chật chội
    padding1 = (new_y1_max - new_y1_min) * 0.05
    padding2 = (new_y2_max - new_y2_min) * 0.05
    y1_range = [new_y1_min - padding1, new_y1_max + padding1]
    y2_range = [new_y2_min - padding2, new_y2_max + padding2]
    
    # THÊM TRACE 1: Lợi nhuận tích lũy (Line chart, trục Y bên trái)
    fig.add_trace(
        go.Scatter(
            x=dates, 
            y=rolling_returns, 
            name=f'Lợi nhuận tích lũy 25 ngày',
            mode='lines',
            line=dict(color='royalblue', width=2),
        ),
        secondary_y=False, # Chỉ định đây là trục Y chính (bên trái)
    )
    
    bar_colors = np.where(money_flow_data >= 0, '#00CC96', '#FF6692') # Xanh lá cây / Đỏ
    # THÊM TRACE 2: Dòng tiền (Bar chart, trục Y bên phải)
    fig.add_trace(
        go.Bar(
            x=dates, 
            y=money_flow_data, 
            name='Dòng tiền hàng ngày',
            marker_color=bar_colors,
            opacity=1,
        ),
        secondary_y=True, # Chỉ định đây là trục Y phụ (bên phải)
    )
    
    # --- BƯỚC 3: TINH CHỈNH LAYOUT, TIÊU ĐỀ VÀ CÁC TRỤC ---
    
    # Cập nhật layout chung
    fig.update_layout(
        title_text=f'Phân tích Lợi nhuận Tích lũy và Dòng tiền Của {stock_fig}',
        template='ygridoff',
        legend=dict(
            orientation="h",
            yanchor="bottom",
            y=1.02,
            xanchor="right",
            x=1
        )
    )
    fig.update_xaxes(title_text ='Thời gian',rangebreaks=[dict(bounds=["sat", "sun"])])
    
    # Cập nhật trục Y bên trái (cho Lợi nhuận)
    fig.update_yaxes(
        title_text="<b>Lợi nhuận Tích lũy</b>",
        secondary_y=False,
        tickformat=".0%", # Định dạng tick kiểu phần trăm
        # color='royalblue'
        range = y1_range
    )
    
    # Cập nhật trục Y bên phải (cho Dòng tiền)
    fig.update_yaxes(
        title_text="<b>Dòng tiền (tỷ VNĐ)</b>",
        secondary_y=True,
        showgrid=False, # Ẩn lưới của trục phụ cho đỡ rối
        # color='lightsalmon'
        range=y2_range
    )
    
    # Hiển thị biểu đồ
    fig.show()
In [50]:
cumulative_returns_vs_inf('CTD')
In [12]:
import nbformat
from nbconvert.exporters import HTMLExporter

# Đọc file notebook
with open("Demo_Backtest.ipynb", encoding="utf-8") as f:
    notebook_node = nbformat.read(f, as_version=4)

# Tạo exporter
html_exporter = HTMLExporter()
html_exporter.exclude_input = True  # ẩn code nếu muốn

# Xuất HTML
(body, resources) = html_exporter.from_notebook_node(notebook_node)

# Lưu kết quả
with open("my_notebook.html", "w", encoding="utf-8") as f:
    f.write(body)

print("✅ Xuất thành công: my_notebook.html")
✅ Xuất thành công: my_notebook.html
In [51]:
fig_stock=fig_investor('CTD','WOLF')
In [54]:
fig_industry = plot_industry_investor_density([stock.upper() for stock in sec_stock],investor='SHEEP')

OTHER¶

In [ ]:
def optimaze_stragedy(n_loop):
    backtest_699={}
    for y in range(n_loop):
        print(y)
        df_adjust = df.copy()
        for stock in estate_con_stock:df_adjust.drop(columns=[(stock.upper(),'NOTE'), (stock.upper(),'NOTE_AUDIT')],inplace=True)
        manager = OptimizedStockTradeManager(df_adjust,
                                             bank_stock,retail_stock, sec_stock,estate_con_stock,
                                             date_begin=data_1['VNINDEX']['DATE'][random.randint(250,1103)].date(),
                                             box_2=pk.load(open('BOX_1','rb')))
        # # Update df using the new method
        # manager.update_df(df)
        backtest_1 = manager.backtest(not_replace=False, save= False)
        backtest_699[f'{y}']=backtest_1
    return backtest_699
# display(backtest_1[0].tail())
# print(backtest_1[0].iloc[:,-1].max())
# Còn 2 thay thế để có thể tăng tốc độ thực thi
## 1/ Ngay từ ban đầu tạo ra các list 
In [ ]:
backtest_699 = optimaze_stragedy(700)
In [ ]:
# pd.set_option('Display.max_rows',None)
backtest_699['9'][0].tail()
In [ ]:
with open('QUANT_SIM_699','rb') as file:
    quant_sim = pk.load(file)
with open('BACKTEST_1706','rb') as file:
    quant_sim_1 = pk.load(file)
In [ ]:
def start_date_4_ssi(start_download = pdts('2020-02-26')):
    vnindex = data_1['VNINDEX']
    index_date = find_index(col_name='DATE', df=vnindex, obj=start_download)
    reference_date = vnindex.loc[0:index_date,
                                 'DATE'].sort_values(ascending=True,
                                                     ignore_index=True)
    return reference_date
In [ ]:
def simulate(stock, i, date_check, seed, percent):
    vnindex_5m = df_pl[stock]

    data_dict = {
        'DATE':  np.array(ref_dates[25:]),
        'Price_min': np.empty(0),
        'Price_10': np.empty(0),
        'Price_25':np.empty(0),
        'Mean_Price': np.empty(0),
        'Price_75': np.empty(0),
        'Price_90': np.empty(0),
        'Price_max': np.empty(0),
        'Close_Price': np.empty(0)
    }
    stock_chosen = data_1[stock]
    dates_value = stock_chosen.iloc[:,0].to_numpy()

    for each_date in ref_dates[25:]:
        vnindex_each_date = vnindex_5m.filter(
            (pl.col('Date') == each_date.date())
        )

        chosen_prices = vnindex_each_date['Ave'].to_numpy()

        if chosen_prices.any():
            percent_values = np.percentile(chosen_prices, percent)
            close_price = vnindex_each_date['Close'][-1]
        else:
            close_price = stock_chosen.iloc[np.where(dates_value==each_date)[0][0], 2]
            percent_values = [close_price] * 7

        data_dict['Price_min']=np.append(data_dict['Price_min'],percent_values[0])
        data_dict['Price_10']=np.append(data_dict['Price_10'],percent_values[1])
        data_dict['Price_25']=np.append(data_dict['Price_25'],percent_values[2])
        data_dict['Mean_Price']=np.append(data_dict['Mean_Price'],percent_values[3])
        data_dict['Price_75']=np.append(data_dict['Price_75'],percent_values[4])
        data_dict['Price_90']=np.append(data_dict['Price_90'],percent_values[5])
        data_dict['Price_max']=np.append(data_dict['Price_max'],percent_values[6])
        data_dict['Close_Price']=np.append(data_dict['Close_Price'],close_price)

    vnindex_prices = pd.DataFrame(data_dict)
    # Chuyển series_ratio sang NumPy array
    series_ratio = (stock_chosen.iloc[:date_check, 2].to_numpy() / stock_chosen.iloc[:date_check, 1].to_numpy())[::-1]

    # Phát triển lại các cột của vnindex_prices thành NumPy array
    vnindex_prices_np = vnindex_prices.iloc[:, 1:].to_numpy()

    # Sử dụng broadcasting để chia tất cả các cột cho series_ratio
    vnindex_prices_np /= series_ratio[:, np.newaxis]

    if stock == 'ACB':
        # Tính toán các chỉ số chỉ một lần
        index_error = find_index(col_name='DATE', df=vnindex_prices, obj=pdts('2022-11-30'))
        index_error_1 = find_index(col_name='DATE', df=vnindex_prices, obj=pdts('2023-05-31'))
        index_ratio = find_index(col_name='DATE', df=stock_chosen, obj=pdts('2023-06-01'))

        # Tính giá trị ratio chỉ một lần
        ratio = stock_chosen.iloc[index_ratio, 2] / stock_chosen.iloc[index_ratio, 1]

        # Thực hiện phép nhân trên NumPy array cho đoạn dữ liệu tương ứng
        vnindex_prices_np[index_error:index_error_1 + 1, :] *= ratio

    # Gán lại giá trị đã được tính toán vào vnindex_prices
    vnindex_prices.iloc[:, 1:] = vnindex_prices_np
    
    rng = np.random.default_rng(seed)

    # Chuyển giá trị của các cột cần thiết sang NumPy array để tăng tốc
    price_array = vnindex_prices.iloc[:, 1:].to_numpy()

    # Tạo mảng xác suất đều nhau cho 8 lựa chọn
    probabilities = np.full(price_array.shape[1], 1 / price_array.shape[1])

    # Chọn ngẫu nhiên chỉ số cho mỗi hàng theo xác suất đã định
    random_indices = rng.choice(price_array.shape[1], size=price_array.shape[0], p=probabilities)

    # Truy xuất giá trị ngẫu nhiên dựa trên chỉ số đã chọn
    backtest_price = np.round(price_array[np.arange(price_array.shape[0]), random_indices], 6)

    vnindex_prices['Backtest_Price'] = backtest_price

#     cols = [
#         f'Price_min_{i}_{stock}', f'Price_10_{i}_{stock}', f'Price_25_{i}_{stock}',
#         f'Mean_Price_{i}_{stock}', f'Price_75_{i}_{stock}', f'Price_90_{i}_{stock}',
#     ]

    vnindex_prices.rename(columns={'Backtest_Price': f'Backtest_Price_{i}_{stock}'}, inplace=True)

# #     for col in cols:
# #         vnindex_prices[f'Return_{col}'] = np.log(vnindex_prices[col].to_numpy() / vnindex_prices[col].shift(1).to_numpy())
#     # Chuyển các cột 'cols' sang NumPy array
#     vnindex_prices_np = vnindex_prices[cols].to_numpy()

#     # Tính toán log returns bằng cách tạo mảng NumPy cho các giá trị trước đó (shift)
#     shifted_np = np.roll(vnindex_prices_np, 1, axis=0)  # Dịch toàn bộ lên một hàng
# #     shifted_np[0, :] = np.nan  # Hàng đầu tiên là NaN

#     # Tính toán log returns
#     log_returns = np.log(vnindex_prices_np / shifted_np, where=shifted_np > 0)  # Tối ưu bằng 'where'

#     # Gán log_returns vào DataFrame với tên các cột mới
#     vnindex_prices[[f'Return_{col}' for col in cols]] = log_returns
    return vnindex_prices[f'Backtest_Price_{i}_{stock}']
In [ ]:
percentiles = [0, 10, 25, 50, 75, 90, 100]
ref_dates = start_date_4_ssi(pdts('2019-02-25'))
date_check=len(ref_dates[25:])
for i in range(10):
    simulate_1=simulate('BID',i,seed=random.randint(0,10000000),date_check=date_check,percent=percentiles)
In [ ]:
ref_dates = start_date_4_ssi(pdts('2019-02-25'))
stock_check=bank_stock+sec_stock+retail_stock+estate_con_stock
stock_check.append('VNINDEX')
# date_begin = pdts('2019-04-01')
# index_date=ref_dates[ref_dates==date_begin].index[0]
# simulate_stocks=pd.DataFrame({'DATE':  ref_dates[index_date:].to_list()})
i = 0
file_pattern = "C:/Users/duytk/data_1m/*.parquet"

with open('ADJUSTED DATA','rb') as file:
    data_1=pk.load(file)
# with open('SIMULATE_STOCK','rb') as file:
#     simulate_stocks=pk.load(file)
date_check=len(ref_dates[25:])
In [ ]:
# VNINDEX data get wrong at 1524
df_1m = pl.scan_parquet(file_pattern,cache=True,parallel='columns')
df_pl={f'{stock.upper()}':df_1m.filter(pl.col('Symbol') == stock.upper()).select(pl.col('Date'),pl.col('Ave'),pl.col('Close')).collect() for stock in stock_check}
df_vnindex=df_pl['VNINDEX']
df_vnindex = df_vnindex.with_columns(pl.when(pl.col('Ave')<10).then(pl.col('Ave')*1000).otherwise(pl.col('Ave')),
                                     pl.when(pl.col('Close')<10).then(pl.col('Close')*1000).otherwise(pl.col('Close'))
                                    )
df_pl['VNINDEX'] = df_vnindex
In [ ]:
df_append = []
percentiles = [0, 10, 25, 50, 75, 90, 100]
number = 700
for i in range(number):
    results = map(
        lambda stock: simulate(
            stock=stock.upper(),
            i=i,
            seed=random.randint(0, 10000000),
            date_check=date_check,
            percent=percentiles,
        ),
        stock_check,
    )
    df_append.extend(results)

# Gộp tất cả các DataFrame thành một
simulate_stocks = pd.concat(df_append, axis=1)
for i in range(number):
    simulate_stocks[f'Backtest_Price_{i}_VNINDEX'][1524]=simulate_stocks[f'Backtest_Price_{i}_VNINDEX'][1524]*10
In [ ]:
def random_simulate(i, data_2=None, simulate_stocks_copy=None):
    # Định nghĩa các cột cần mô phỏng
    cols_simulate = ['AVE_PRICE', 'MODIFY_PRICE', 'OPEN', 'HIGH', 'LOW']

    for stock in stock_check:
        stock = stock.upper()

        # Chuyển dữ liệu từ DataFrame sang NumPy array để xử lý
        stock_data = data_2[stock]
        stock_data = stock_data.iloc[1:].reset_index(drop=True)  # Bỏ hàng đầu tiên và reset index
        data_2[stock] = stock_data  # Cập nhật lại DataFrame

        # Lấy dữ liệu mô phỏng từ simulate_stocks_copy
        ave_stock = simulate_stocks_copy.loc[:, f"Backtest_Price_{i}_{stock}"][::-1].reset_index(drop=True).to_numpy()

        # Tính độ dài cần thay đổi và chuyển đổi dữ liệu gốc thành NumPy arrays
        len_change = len(ave_stock) - 1
        ave_ori = stock_data.loc[:len_change, 'AVE_PRICE'].to_numpy()
        
        # Tính tỷ lệ
        ratio = ave_stock / ave_ori

        # Sử dụng NumPy để áp dụng tỷ lệ cho các cột cần thiết
        stock_data_np = stock_data.loc[:len_change, cols_simulate].to_numpy()
        stock_data_np /= ratio[:, np.newaxis]
        
        # Cập nhật lại vào DataFrame
        data_2[stock].loc[:len_change, cols_simulate] = stock_data_np
    
    return data_2
def simulate_porfolio(i,simulate_stocks_copy=None):
    
    with open('BOX_1','rb') as file:
        box_2=pk.load(file)
    with open('ADJUSTED DATA','rb') as file:
        data_2=pk.load(file)
#     with open('SIMULATE_STOCK_100','rb') as file:
#         sim_100=pk.load(file)
# Delete last day at box_2
    del box_2[pdts('2025-05-22')]
    
    data_1=random_simulate(i,data_2=data_2,simulate_stocks_copy=simulate_stocks_copy)
#     vnindex=data_1['VNINDEX']
    
    df = total_df(bank_stock, retail_stock, sec_stock, index_funds,data_1=data_1)
# Drop data at last day
    df = df.drop(labels=[len(df)-1])
#     df = df.drop(labels=[1219])
    df = df.reset_index(drop=True)
    vnindex = data_2['VNINDEX']
    date_begin = vnindex['DATE'][random.randint(250,1089)]
    manager = StockTradeManager(df,bank_stock,retail_stock, sec_stock,estate_con_stock,date_begin=date_begin,box_2=box_2)

    # Update df using the new method
#     manager.update_df(df)

    backtest_1 = manager.backtest(not_replace=False)

    return backtest_1
In [ ]:
def df_beta_sec(*args,
                list_sec,
                periods=250,
                start_date=pd.Timestamp('2016-01-01'),
                portfolio=False,
                price_type='AVE_PRICE',
                data_1=None):
    global data_concat
    data = data_concat if 'data_concat' in globals() else data_1
    vnindex_1 = data['VNINDEX']
    vnindex_1 = vnindex_1[vnindex_1['DATE'] > start_date]
    vnindex_1 = vnindex_1.sort_values('DATE').reset_index(drop=True)
    ave_vn = vnindex_1['MODIFY_PRICE'].values
    return_vn = np.diff(ave_vn) / ave_vn[:-1]
    length_vn = len(return_vn)
    box_cov = {'DATE': vnindex_1['DATE'].values[1:]}

    if portfolio:
        list_sec = args

    for stock in list_sec:
        stock = stock.upper() if not portfolio else stock
        stock_1 = data[stock] if not portfolio else stock
        stock_1 = stock_1[stock_1['DATE'] > start_date]
        stock_1 = stock_1.sort_values('DATE').reset_index(drop=True)
        price_values = stock_1[price_type].values
        return_stock = np.diff(price_values) / price_values[:-1]
        length_stock = len(return_stock)
        beta_box = np.full(length_vn, np.nan)
        miss_len = 0 if length_stock == length_vn else length_vn - length_stock

        # Use sliding window for covariance and variance calculations
        if length_stock >= periods + 1:
            rolling_returns_stock = sliding_window_view(return_stock, window_shape=periods + 1)
            rolling_returns_vn = sliding_window_view(return_vn[miss_len:], window_shape=periods + 1)
#             covariances = np.array([np.cov(rolling_returns_stock[i], rolling_returns_vn[i])[0, 1]
#                                     for i in range(len(rolling_returns_stock))])
            # Tính số phần tử trong mỗi cửa sổ
            n = rolling_returns_stock.shape[1]

            # Tính mean cho từng cửa sổ
            mean_stock = np.mean(rolling_returns_stock, axis=1)
            mean_vn = np.mean(rolling_returns_vn, axis=1)

            # Tính covariance bằng phép toán ma trận
            covariances = (
                np.sum(rolling_returns_stock * rolling_returns_vn, axis=1) -
                n * mean_stock * mean_vn
            ) / (n - 1)
            variances_vn = np.var(rolling_returns_vn, axis=1)
            betas = covariances / variances_vn

            # Assign calculated betas to the beta_box array
            beta_box[periods + miss_len:length_vn] = betas

        box_cov[f'BETA_{stock}' if not portfolio else f'BETA_PORTFOLIO'] = beta_box

    df_beta = pd.DataFrame(box_cov)
    df_beta = df_beta[df_beta['DATE'] > pd.Timestamp('2020-02-13')].reset_index(drop=True)
    return df_beta
def df_brand_index(dict_brand, price_type='AVE_PRICE',data_1=data_1):
    global issue_1
    global brands_index
    vnindex = data_1['VNINDEX']
    vnindex_1 = vnindex[vnindex.iloc[:, 0] > pdts('2020-02-13')].reset_index(drop=True)
    vnindex_date = vnindex_1.iloc[:, 0].values[::-1]
    all_dfs = []

    for brand, list_stock in dict_brand.items():
        stock_data = {stock: data_1[stock.upper()] for stock in list_stock}
        stock_issues = {stock: issue_1[stock].iloc[0, -1] for stock in list_stock}

        # Chuyển đổi dữ liệu từng cổ phiếu sang NumPy để tính toán hàng loạt
        stock_arrays = {
            stock: np.array([
                stock_data[stock]['DATE'].values,
                stock_data[stock]['MODIFY_PRICE'].values,
                stock_data[stock]['OPEN'].values,
                stock_data[stock]['HIGH'].values,
                stock_data[stock]['LOW'].values,
                stock_data[stock]['AVE_PRICE'].values if price_type == 'AVE_PRICE' else None
            ])
            for stock in list_stock
        }

        # Khởi tạo mảng lưu kết quả
        list_index = ['INDEX_O', 'INDEX_C', 'INDEX_A', 'INDEX_H', 'INDEX_L']
        brand_values = {key: np.zeros(len(vnindex_date)) for key in list_index }

        # Tính toán chỉ số theo từng ngày
        for i, date in enumerate(vnindex_date):
            for stock, data in stock_arrays.items():
                idx = np.where(data[0] == date)[0]
                if idx.size == 0:continue
                
                c_price, o_price, h_price, l_price = data[1:5, idx[0]]
                a_price = data[5, idx[0]] if price_type == 'AVE_PRICE' else np.mean(price_type)

                issue = stock_issues[stock]
                brand_values['INDEX_C'][i] += c_price * issue
                brand_values['INDEX_O'][i] += o_price * issue
                brand_values['INDEX_A'][i] += a_price * issue
                brand_values['INDEX_H'][i] += h_price * issue
                brand_values['INDEX_L'][i] += l_price * issue

        # Tính chỉ số theo phần trăm so với giá trị đầu tiên
        for key in brand_values:brand_values[key] /= brand_values[key][0]

        # Tính tỷ suất lợi nhuận (log-return)
        brand_index = pd.DataFrame(brand_values)
        for key in list_index:brand_index[f'RETURN_{key[-1]}'] = np.log(brand_index[key] / brand_index[key].shift(1))

        # Tính MACD (có thể batch process nếu `macd_hvix` hỗ trợ)
        macd_results = {
            key: macd_hvix(pd.concat([vnindex_1.iloc[:, 0], brand_index[key], brand_index[f'RETURN_{key[-1]}']], axis=1),
                           pd_dataframe=False, brand=brand).rename(columns={'AVE_PRICE':key})
            for key in list_index
        }
        brand_index = pd.DataFrame()
        # Gộp kết quả MACD vào DataFrame
        for key, macd_result in macd_results.items():
            macd_result.rename(columns={col: f'{col}_{key[-1]}' for col in ['DIFF', 'DEA', 'OSC']}, inplace=True)
            brand_index = pd.concat([brand_index, macd_result.iloc[:,1:]], axis=1)
        

#         brand_index = pd.concat([brand_index['INDEX_O'], brand_index['RETURN_O'], macd_results['INDEX_O'][brand]['DIFF_O'],
#                                 macd_results['INDEX_O'][brand]['DEA_O'], macd_results['INDEX_O'][brand]['OSC_O'],
#                                 brand_index['INDEX_C'], brand_index['RETURN_C'], macd_brand_c[brand.upper()]['DIFF_C'],
#                                 macd_brand_c[brand.upper()]['DEA_C'], macd_brand_c[brand.upper()]['OSC_C'],
#                                 brand_index['INDEX_A'], brand_index['RETURN_A'], macd_brand_a[brand.upper()]['DIFF_A'],
#                                 macd_brand_a[brand.upper()]['DEA_A'], macd_brand_a[brand.upper()]['OSC_A'],
#                                 brand_index['INDEX_H'], brand_index['RETURN_H'], macd_brand_h[brand.upper()]['DIFF_H'],
#                                 macd_brand_h[brand.upper()]['DEA_H'], macd_brand_h[brand.upper()]['OSC_H'],
#                                 brand_index['INDEX_L'], brand_index['RETURN_L'], macd_brand_l[brand.upper()]['DIFF_L'],
#                                 macd_brand_l[brand.upper()]['DEA_L'], macd_brand_l[brand.upper()]['OSC_L']
#                                 ], axis=1)
        
        brand_index.columns = pd.MultiIndex.from_product([[f'{brand.upper()}_INDEX'], brand_index.columns])
        all_dfs.append(brand_index)

    # Gộp tất cả thương hiệu vào một DataFrame
    brands_index = pd.concat(all_dfs, axis=1)
    return brands_index
def macd_hvix(stock, pd_dataframe=True, brand=None, portfolio=None, price_type='AVE_PRICE', data_1=data_1):
    global max_time, data_concat

    data = data_concat if 'data_concat' in globals() else data_1
    time_series = [29, 13]
    p = 9
    ratio_1 = 9.5125 / 13
    ratio_2 = 1 - ratio_1
    stock = stock.upper() if pd_dataframe else stock

    if pd_dataframe:
        df_stock = data[stock]
        m_price = df_stock['MODIFY_PRICE'].values[::-1]
        if price_type == 'AVE_PRICE':a_price = df_stock['AVE_PRICE'].values[::-1] if stock.upper() != 'VNINDEX' else m_price
        else:
            prices_type = [df_stock[type_p].values for type_p in price_type]
            a_price = np.mean(prices_type, axis=0)[::-1]
        l_price = df_stock['LOW'].values[::-1]
        o_price = df_stock['OPEN'].values[::-1]
        h_price = df_stock['HIGH'].values[::-1]
        date = df_stock['DATE'].values[::-1]
        return_1 = np.diff(np.log(a_price), prepend=np.nan)
        
        
        # Filter data based on start_date outside the loop
        start_date = pdts('2014-03-31') if not portfolio else pdts('2018-01-01')
        valid_indices = np.where(date > start_date)[0]
        
        m_price = m_price[valid_indices]
        a_price = a_price[valid_indices]
        l_price = l_price[valid_indices]
        o_price = o_price[valid_indices]
        h_price = h_price[valid_indices]
        date = date[valid_indices]
        return_1 = return_1[valid_indices]
        
        df = {
            'DATE': date,
            'AVE_PRICE': a_price,
            'RETURN': return_1,
            'MODIFY_PRICE': m_price,
            'OPEN': o_price,
            'HIGH': h_price,
            'LOW': l_price
        }
    else:
        df = pd.DataFrame(stock)
        old_name = df.columns
        df.rename(columns={old_name[1]: 'AVE_PRICE', old_name[2]: 'RETURN'}, inplace=True)
        start_date = pdts('2014-03-31') if not portfolio else pdts('2018-01-01')
        df = df[df['DATE'] > start_date].reset_index(drop=True)
        return_1 = df['RETURN'].values
    
    length = len(df['RETURN'])
    
    for time in time_series:
        stds = np.full(length, np.nan)
        alpha = np.full(length, np.nan)
        ema = np.full(length, np.nan)
        
        # Tính toán cửa sổ trượt
        rolling_returns = sliding_window_view(return_1[1:], window_shape=time-1)

        # Tính trung bình của mỗi cửa sổ
        mean_returns = np.mean(rolling_returns, axis=1)

        # Tính độ lệch tuyệt đối
        abs_deviation = np.abs(rolling_returns - mean_returns[:,None])

        # Tính stds với trung bình độ lệch tuyệt đối
        stds[time-1:] = np.mean(abs_deviation, axis=1)
#         for j in range(time*2-2,length-1):
#             alpha[j] = stds[j] / np.nansum(stds[(j - time + 1):(j + 1)])
#         print(alpha)
            
        sliding_windows = sliding_window_view(stds[time-1:], window_shape=time)

        # Tính tổng của từng cửa sổ trượt (bỏ qua NaN)
        window_sums = np.nansum(sliding_windows, axis=1)
        j = time*2-2
        # Áp dụng logic để tính alpha
        alpha[j:length-1] = stds[j:length-1] / window_sums[:length - time*2 + 1]

            
        ema[j] = df['AVE_PRICE'][j]
        for j in range(time*2-2, length-1):ema[j+1] = (1 - alpha[j]) * ema[j] + df['AVE_PRICE'][j+1] * alpha[j]
            
        df[f'STD_{time}'] = stds
        df[f'ALPHA_{time}'] = alpha
        df[f'EMA_{time}'] = ema

    df['DIFF'] = df[f'EMA_{min(time_series)}'] - df[f'EMA_{max(time_series)}']
    max_time = max(time_series)
    dea = np.full(length, np.nan)
    dea[max_time * 2 + p - 2] = np.mean(df['DIFF'][(max_time * 2 - 1):(max_time * 2 + p - 2)])

    for j in range(max_time * 2 + p - 1, length):dea[j] = dea[j-1] * ratio_1 + df['DIFF'][j] * ratio_2

    df['DEA'] = dea
    df['OSC'] = df['DIFF'] - df['DEA']

    if pd_dataframe:
        cols=np.array(['DATE', 'AVE_PRICE', 'RETURN', 'MODIFY_PRICE', f'STD_{max(time_series)}', f'EMA_{min(time_series)}', f'EMA_{max(time_series)}', 'DIFF', 'DEA', 'OSC', 'OPEN', 'LOW', 'HIGH'])
        df_final = pd.concat([
                pd.Series(df['DATE'], name='DATE'),
                pd.Series(df['AVE_PRICE'].round(3), name='AVE_PRICE'),
                pd.Series(df['RETURN'].round(3), name='RETURN'),
                pd.Series(df['MODIFY_PRICE'].round(3), name='MODIFY_PRICE'),
                pd.Series(df[f'STD_{max(time_series)}'].round(3), name=f'STD_{max(time_series)}'),
                pd.Series(df[f'EMA_{min(time_series)}'], name=f'EMA_{min(time_series)}'),
                pd.Series(df[f'EMA_{max(time_series)}'], name=f'EMA_{max(time_series)}'),
                pd.Series(df['DIFF'].round(5), name='DIFF'),
                pd.Series(df['DEA'].round(5), name='DEA'),
                pd.Series(df['OSC'].round(5), name='OSC'),
                pd.Series(df['OPEN'].round(3), name='OPEN'),
                pd.Series(df['LOW'].round(3), name='LOW'),
                pd.Series(df['HIGH'].round(3), name='HIGH')
            ], axis=1)
        
        df_final.columns = pd.MultiIndex.from_product([[stock],cols ])
    else:
        df_final = pd.concat([
                pd.Series(df['DATE'], name='DATE'),
                pd.Series(df['AVE_PRICE'].round(3), name='AVE_PRICE'),
                pd.Series(df['RETURN'].round(3), name='RETURN'),
                pd.Series(df[f'STD_{max(time_series)}'].round(3), name=f'STD_{max(time_series)}'),
                pd.Series(df['DIFF'].round(5), name='DIFF'),
                pd.Series(df['DEA'].round(5), name='DEA'),
                pd.Series(df['OSC'].round(5), name='OSC'),
            ], axis=1)
#         df_final.columns = pd.MultiIndex.from_product([[brand.upper()], np.array(['DATE', old_name[1], old_name[2], f'STD_{max_time}', 'DIFF', 'DEA', 'OSC'])])
    return df_final

def big_df_macd(total_list, portfolio=False, drop_date=True, time_default=pdts('2020-02-13'), price_type='AVE_PRICE',data_1=data_1):
    global column_macd
    column_macd = ['AVE_PRICE', 'RETURN', 'MODIFY_PRICE', f'STD_29', 'DIFF', 'DEA', 'OSC', 'OPEN', 'LOW', 'HIGH']
    
    all_macds = []
    for stock in total_list:
        macd = macd_hvix(stock, portfolio=portfolio, price_type=price_type,data_1=data_1)
        time_compare=macd.iloc[:,0].values
        if drop_date:macd = macd[time_compare > time_default].reset_index(drop=True).drop(columns = (stock.upper(), 'DATE'))
        else:macd = macd[time_compare > time_default].reset_index(drop=True)
        all_macds.append(macd)
    
    df_macd = pd.concat(all_macds, axis=1)
    return df_macd
# import pandas as pd
# import numpy as np
# from pandas import to_datetime as pdts
# from pandas.tseries.offsets import DateOffset


def total_df(list_bank, list_retail, list_sec, index_funds,data_1=data_1):
    vnindex=data_1['VNINDEX']
    # Set stock 
    total_list = list_bank + list_retail + list_sec + index_funds
    esta_con={'ESTA_CON':[stock.lower() for stock in estate_con]}
    dict_brand = {'bank': list_bank, 'sec': list_sec, 'retail': list_retail}
    # Beta
    df_beta = df_beta_sec(list_sec=list_sec,data_1=data_1)
    df_beta_bank = df_beta_sec(list_sec=list_bank, periods=150,data_1=data_1)
    df_beta_retail = df_beta_sec(list_sec=list_retail, periods=150,data_1=data_1)
    df_beta_esta=df_beta_sec(list_sec=esta_con['ESTA_CON'],periods=150,data_1=data_1)
    # MACD
    df_macd = big_df_macd(total_list,data_1=data_1)
    macd_esta_con=big_df_macd(estate_con,price_type='AVE_PRICE',data_1=data_1)
    # Score
    df_banks = df_score_banks(list_bank)
    df_retails = df_score_retails(list_retail)
    df_estate = df_score_estate()
    # Brand index
    brands_index = df_brand_index(dict_brand,data_1=data_1)
    estate_index=df_brand_index(esta_con,price_type='AVE_PRICE',data_1=data_1)
    # Get concat
    date = pd.DataFrame(vnindex[vnindex.iloc[:, 0] > pdts(
        '2020-02-13')].iloc[:, 0])[::-1].reset_index(drop=True)
    date.columns = pd.MultiIndex.from_tuples([('DATE', 'DATE')])
    df = pd.DataFrame(columns=pd.MultiIndex.from_product([[], column_macd +
                                                          column_score]))
    df = pd.concat([df, date], axis=1)
    # Bank
    df = pd.concat([df] + [
        pd.concat([
            df_beta_bank[f'BETA_{stock.upper()}'],df_macd[stock.upper()], 
            df_banks[stock.upper()]
        ],
                  axis=1,
                  keys=[stock.upper(),
                        stock.upper(),
                        stock.upper()]) for stock in list_bank
    ],
                  axis=1)
    # Retail
    df = pd.concat([df] + [
        pd.concat([
            df_beta_retail[f'BETA_{stock.upper()}'],df_macd[stock.upper()], 
            df_retails[stock.upper()]
        ],
                  axis=1,
                  keys=[stock.upper(),
                        stock.upper(),
                        stock.upper()]) for stock in list_retail
    ],
                   axis=1)
    # Sec
    df = pd.concat([df] + [
        pd.concat([df_beta[f'BETA_{stock.upper()}'], df_macd[stock.upper()]],
                  axis=1,
                  keys=[stock.upper(), stock.upper()]) for stock in list_sec
    ],
                   axis=1)
    # Estate
    df=pd.concat([df]+[
        pd.concat([
            df_beta_esta[f'BETA_{stock.upper()}'],macd_esta_con[stock.upper()], 
            df_estate[stock.upper()]
        ],
                  axis=1,
                  keys=[stock.upper(),
                        stock.upper(),
                        stock.upper()]) for stock in estate_con
    ],axis=1)
    df=pd.concat([df,estate_index],axis=1)

    df = pd.concat([df, df_macd.loc[:, ['VNINDEX']], brands_index], axis=1)
    return df
In [ ]:
# Chuẩn bị từ điển DataFrame rỗng trước
stock_check=bank_stock+sec_stock+retail_stock+estate_con_stock
stock_check = [stock.upper() for stock in stock_check]
dict_df = {stock: pd.DataFrame(columns=[
    'BUY_DATE', 'SELL_DATE', 'STOCK', 'KEY_PORFOLIO', 'N_PORFOLIO',
    'BUY_PRICE', 'SELL_PRICE', 'PROFIT'
]) for stock in stock_check}

# Gộp toàn bộ dữ liệu về một DataFrame thay vì xử lý theo từng phần
all_data = []

for keys, porfolio in quant_sim.items():
    for i in range(1, 6):
        # Lấy sub_porfolio có SELL_DATE hợp lệ
        sub_porfolio = porfolio[i].dropna(subset=['SELL_DATE'])

        # Thêm KEY_PORFOLIO và N_PORFOLIO
        sub_porfolio = sub_porfolio.assign(
            KEY_PORFOLIO=keys,
            N_PORFOLIO=i
        )

        # Chỉ giữ các dòng liên quan đến stock_check
        sub_porfolio = sub_porfolio[sub_porfolio['STOCK'].isin(stock_check)]

        # Chọn các cột cần thiết
        sub_porfolio = sub_porfolio[[
            'BUY_DATE', 'SELL_DATE', 'STOCK', 'KEY_PORFOLIO', 'N_PORFOLIO',
            'BUY_PRICE', 'SELL_PRICE', 'PROFIT'
        ]]

        # Thêm vào danh sách tổng hợp
        all_data.append(sub_porfolio)

# Hợp nhất toàn bộ dữ liệu
merged_data = pd.concat(all_data, ignore_index=True)

# Phân tách dữ liệu theo từng stock và lưu vào dict_df
for stock in stock_check:
    stock_data = merged_data[merged_data['STOCK'] == stock]
    if not stock_data.empty:
        dict_df[stock] = pd.concat([dict_df[stock], stock_data], ignore_index=True)
In [ ]:
# Mean
# Std
# Distance
# Outlier
# Quantity
vnindex_date=vnindex['DATE'].values
for stock in stock_check:
    stock_df=dict_df[stock]
    stock_df['YEAR'] = [buy_date.year for buy_date in stock_df['BUY_DATE']]
    stock_df['COLOR'] = np.where(stock_df['PROFIT'] >= 1, 'Positive', 'Negative')
    buy_dates=stock_df['BUY_DATE'].values
    sell_dates=stock_df['SELL_DATE'].values
    distance=np.zeros(np.shape(buy_dates)[0])
    for index_date ,dates in enumerate(zip(buy_dates,sell_dates)):
        index_buy=np.where(vnindex_date==dates[0])[0][0]
        index_sell=np.where(vnindex_date==dates[1])[0][0]
        distance[index_date]=index_buy-index_sell+2
    stock_df.insert(10,"DISTANCE",distance)
    dict_df[stock] = stock_df
In [ ]:
from plotly.io import to_json, from_json
import json
stock_chosen=dict_df['STB']
fig_scatter = px.scatter(stock_chosen,y='DISTANCE',x='PROFIT',size='DISTANCE',color='COLOR',symbol='YEAR',marginal_x='histogram')

# fig = px.scatter(
#     stock_chosen,
#     x='DISTANCE',
#     y='PROFIT',
#     size='DISTANCE',
#     color='COLOR',
#     symbol='YEAR'
# )

def update_annotations(fig, stock_chosen):
    visible_profits = []

    # Duyệt qua từng trace và kiểm tra trạng thái visible
    for trace in fig.data:
        if trace.visible is None or trace.visible:  # Nếu trace đang hiển thị
            # Lấy chỉ số của trace trong customdata
            indices = trace.customdata
            visible_profits.exxtend(stock_chosen.iloc[indices]["PROFIT"].values)

    # Tính số lượng giá trị dương và âm
    positive_count = sum(1 for p in visible_profits if p >= 1)
    negative_count = sum(1 for p in visible_profits if p < 1)

    # Xóa annotations cũ
    fig.update_layout(annotations=[])

    # Thêm annotation cho số lượng giá trị dương
    fig.add_annotation(
        text=f"Positive: {positive_count}",
        xref="paper",
        yref="paper",
        x=0.95,
        y=0.95,
        showarrow=False,
        font=dict(size=14, color="blue")
    )

    # Thêm annotation cho số lượng giá trị âm
    fig.add_annotation(
        text=f"Negative: {negative_count}",
        xref="paper",
        yref="paper",
        x=0.95,
        y=0.90,
        showarrow=False,
        font=dict(size=14, color="red")
    )


# Gắn `customdata` để theo dõi chỉ số của từng trace
for i, trace in enumerate(fig.data):
    indices = stock_chosen[stock_chosen['COLOR'] == trace.name].index.tolist()
    trace.customdata = indices

# Cập nhật annotations ban đầu
update_annotations(fig, stock_chosen)
In [ ]:
args_1 = [
                {'visible': [stock_code == s for s in stock_duplicate]},
                {'title': f"Biểu đồ Scatter cho cổ phiếu {stock_code}", 'title_x': 0.5},
                {f'{key_axis[0]}':domain_x},
                {f'{key_axis[1]}':domain_y},
                {f'{key_axis[2]}':domain_x2},
                {f'{key_axis[3]}':domain_y2},
                
                
            ]
In [ ]:
# from plotly.offline import plot
# Tạo danh sách các figures cho mỗi cổ phiếu
# Tạo figure
class NpEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        if isinstance(obj, np.floating):
            return float(obj)
        if isinstance(obj, np.ndarray):
            return obj.tolist()
        return super(NpEncoder, self).default(obj)
fig_total = make_subplots(rows=len(dict_df.keys()),cols=2,)
i = 1
positive = {}
negative = {}
range_year = [year for year in dict_df['ACB']['YEAR'].unique()]
table_fil_1 = {
    f'{stock}': {
        f'{year}': {'POS': None, 'NEG': None}
        for year in range_year
    }
    for stock in dict_df.keys()
}
for stock_code, stock_chosen in dict_df.items():
    fig_scatter = px.scatter(
        stock_chosen,
        x='PROFIT',
        y='DISTANCE',
        size='DISTANCE',
        color='COLOR',
        symbol='YEAR',
        marginal_x='box',
        title=f"Biểu đồ Scatter cho cổ phiếu {stock_code}",
    )

    
    positive_count = stock_chosen[stock_chosen['PROFIT'] >= 1]['PROFIT'].count()
    negative_count = stock_chosen[stock_chosen['PROFIT'] < 1]['PROFIT'].count()
    positive[stock_code] = positive_count
    negative[stock_code] = negative_count
    fig_scatter.add_annotation(
        x=0.5,
        y=1.15,
        xref="paper",
        yref="paper",
        text=f"Tổng số lượng giao dịch dương: {positive_count}",
        showarrow=False,
        font=dict(color="blue", size=12),
        xanchor='center',
        yanchor='top'
    )

    fig_scatter.add_annotation(
        x=0.5,
        y=1.08,
        xref="paper",
        yref="paper",
        text=f"Tổng số lượng giao dịch âm: {negative_count}",
        showarrow=False,
        font=dict(color="red", size=12),
        xanchor='center',
        yanchor='top'
    )
    
    for count,trace in enumerate(fig_scatter.data):
        if count%2 == 0:fig_total.add_trace(trace,row=i,col=1)
        else:fig_total.add_trace(trace,row=i,col=2)
    i+=1
    
    for year_uni in range_year:
        table_fil_1[f"{stock_code}"][f"{year_uni}"]['POS']=stock_chosen[(stock_chosen['YEAR']==year_uni)&(stock_chosen['PROFIT']>=1)]['PROFIT'].count()
        table_fil_1[f"{stock_code}"][f"{year_uni}"]['NEG']=stock_chosen[(stock_chosen['YEAR']==year_uni)&(stock_chosen['PROFIT']<1)]['PROFIT'].count()
        
stock_duplicate = [value for value in dict_df.keys() for _ in range(16)]
example_axis={f'{stock_duplicate[0]}':{"xaxis": {'domain':[0,0.001],'showticklabels':False},
                                       "yaxis": {'domain':[0,0.001],'showticklabels':False},
                                       "xaxis2": {'domain':[0,0.001],'showticklabels':False},
                                       "yaxis2": {'domain':[0,0.001],'showticklabels':False}
                                        
                         }
             }
len_range=[i*2 for i in range(0,len(dict_df.keys())-1)]
stock_axis={**example_axis,
            **{f'{stock}':{f"xaxis{i+3}":{'domain':[0,0.001],'showticklabels':False},
                           f"yaxis{i+3}":{'domain':[0,0.001],'showticklabels':False},
                           f"xaxis{i+4}":{'domain':[0,0.001],'showticklabels':False},
                           f"yaxis{i+4}":{'domain':[0,0.001],'showticklabels':False}
                       } for i,stock in zip(len_range,list(dict_df.keys())[1:])}}
# buttons=[]
# for i, stock_code in enumerate(dict_df_2.keys()):
#     stock_axis_adjust=stock_axis[stock_code]
#     key_axis = list(stock_axis_adjust)
#     domain_x = [0, 1]
#     domain_y = [0, 0.5]
#     domain_x2 = [0,1]
#     domain_y2 = [0.5,1]
#     other_axis = [
#     {key: value}
#     for other_stock in stock_axis if other_stock != stock_code
#     for key, value in stock_axis[other_stock].items()
#     ]
#     args_1 = [
#                 {'visible': [stock_code == s for s in stock_duplicate]},
#                 {'title': f"Biểu đồ Scatter cho cổ phiếu {stock_code}", 'title_x': 0.5},
#                 {f'{key_axis[0]}':{'domain':domain_x}},
#                 {f'{key_axis[1]}':{'domain':domain_y}},
#                 {f'{key_axis[2]}':{'domain':domain_x2}},
#                 {f'{key_axis[3]}':{'domain':domain_y2}},                
#             ] + other_axis
# #     args_1.extend(other_axis)
#     buttons.append(
#         dict(
#             label=stock_code,
#             method='update',
#             args=args_1
#         )
#     )
buttons = []
for i, stock_code in enumerate(dict_df.keys()):
    stock_axis_adjust = stock_axis[stock_code]
    key_axis = list(stock_axis_adjust)
    domain_x = [0, 1]
    domain_y = [0, 0.5]
    domain_x2 = [0, 1]
    domain_y2 = [0.6, 1]
    other_axis = [
        {key: value}
        for other_stock in stock_axis
        if other_stock != stock_code
        for key, value in stock_axis[other_stock].items()
    ]

    # Nhóm các cập nhật layout vào một dictionary duy nhất
    layout_update = {
        'title': f"Biểu đồ Scatter cho cổ phiếu {stock_code}",
        'title_x': 0.5,
        f'{key_axis[0]}': {'domain': domain_x},
        f'{key_axis[1]}': {'domain': domain_y},
        f'{key_axis[2]}': {'domain': domain_x2,'showticklabels':False,'opacity':1.2},
        f'{key_axis[3]}': {'domain': domain_y2,'showticklabels':False},
        'annotations':[
                      dict(
                            x=0.05,  # Vị trí x (tính theo phần trăm của trục x)
                            y=0.45,  # Vị trí y (tính theo phần trăm của trục y)
                            text=f"Tổng số lượng giao dịch dương: {positive[stock_code]}",
                            showarrow=False,
                            xref="paper",  # Hệ tọa độ paper (tính theo phần trăm của figure)
                            yref="paper",
                            xanchor='left',
                            yanchor='top',
                            font=dict(color="blue", size=12) # Định dạng font chữ
                            ),
                      dict(
                            x=0.05,
                            y=0.40,
                            text=f"Tổng số lượng giao dịch âm: {negative[stock_code]}",
                            showarrow=False,
                            xref="paper",
                            yref="paper",
                            xanchor='left',
                            yanchor='top',
                            font=dict(color="red", size=12) # Định dạng font chữ
                            ),
                                ]
        
    }

    # Thêm các cập nhật từ other_axis vào layout_update
    for axis_update in other_axis:
        layout_update.update(axis_update)

    args_1 = [
        {'visible': [stock_code == s for s in stock_duplicate]},
        layout_update,  # Cập nhật layout
    ]
    
    buttons.append(
        dict(
            label=stock_code,
            method='update',
            args=args_1,
        )
    )
# Cấu hình updatemenus
updatemenus = [
    dict(
        buttons=buttons,
        direction="down",
        showactive= True,
        x=0.1,
        xanchor="left",
        y=1.25,
        yanchor="top"
    )
]
for i in range(16,len(fig_total.data)):
    fig_total.data[i].visible=False
fig_total.update_layout(updatemenus=updatemenus,    
                        width=1200,  # Chiều rộng mong muốn (đơn vị: pixel)
                        height=900,  # Chiều cao mong muốn (đơn vị: pixel)
                        xaxis1 = dict(domain = [0, 1],showticklabels= False), # Hiển thị đồ thị rug ở góc trên bên trái
                        yaxis1 = dict(domain = [0, 0.5],showticklabels= False), # Hiển thị đồ thị rug ở góc trên bên trái
                        xaxis2 = dict(domain = [0, 1],showticklabels= False), # Hiển thị đồ thị rug ở góc trên bên trái
                        yaxis2 = dict(domain = [0.6, 1],showticklabels= False), # Hiển thị đồ thị rug ở góc trên bên trái
                       )
for i in range(3, len(dict_df.keys())*2):
    fig_total.update_layout({
        f'xaxis{i}': {'domain':[0,0.001],'showticklabels': False, 'showgrid': False, 'zeroline': False, 'visible': False},
        f'yaxis{i}': {'domain':[0,0.001],'showticklabels': False, 'showgrid': False, 'zeroline': False, 'visible': False}
    })
plot_div = plot(fig_total, output_type='div', include_plotlyjs=True)
# Chuyển đổi table_fil_1 thành JSON
table_fil_json = json.dumps(table_fil_1,cls=NpEncoder)
js_code = f"""
<script>
    var table_fil = {table_fil_json};
    var plot_div = document.getElementsByClassName('plotly-graph-div js-plotly-plot')
    var comment_div = document.getElementById('comment-content');
    var active_trace = 'ACB';  // Ban đầu hiển thị comment của biểu đồ đầu tiên

    function updateComment(traceName) {{
        comment_div.innerHTML = comments[traceName];
    }}
    console.log(table_fil)
    function updateTable(traceName) {{
        // Cập nhật bảng
        document.getElementById('positive-2021').innerText = table_fil[traceName]['2021']['POS'];
        document.getElementById('negative-2021').innerText = table_fil[traceName]['2021']['NEG'];
        document.getElementById('positive-2022').innerText = table_fil[traceName]['2022']['POS'];
        document.getElementById('negative-2022').innerText = table_fil[traceName]['2022']['NEG'];
        document.getElementById('positive-2023').innerText = table_fil[traceName]['2023']['POS'];
        document.getElementById('negative-2023').innerText = table_fil[traceName]['2023']['NEG'];
        document.getElementById('positive-2024').innerText = table_fil[traceName]['2024']['POS'];
        document.getElementById('negative-2024').innerText = table_fil[traceName]['2024']['NEG'];
    }}

    // Lắng nghe sự kiện mousedown trên các nút bấm của dropdown menu
    function attachDropdownListener() {{
        // Create a new MutationObserver instance
        var observer = new MutationObserver(function (mutations) {{
            // Check for the buttons in the DOM
            var buttons = document.querySelectorAll("g.updatemenu-dropdown-button-group .updatemenu-dropdown-button");
            if (buttons.length > 0) {{
                buttons.forEach(function (button) {{
                    if (!button.hasAttribute('listener-attached')) {{ // Prevent duplicate listeners
                        button.addEventListener('mousedown', function (event) {{
                            var clickedButton = event.currentTarget;
                            // Retrieve related text
                            var relatedText = click-edButton.querySelector("text");
                            console.log("🚀 ~ relatedText:", relatedText)
                            if (relatedText && relatedText.textContent) {{
                                var traceName = relatedText.textContent.trim();
                                console.log(traceName)
                                // updateComment(traceName);  Update the comment with the correct text
                            
                                updateTable(traceName);
                            }}
                        }});

                        // Mark the button as having a listener
                        button.setAttribute('listener-attached', 'true');
                    }}
                }});
            }}
        }});


        // Configuration for the observer
        var config = {{
            childList: true,
            subtree: true
        }};

        // Start observing the target node for configured mutations
        var plot_div = document.getElementsByClassName('plotly-graph-div js-plotly-plot')[0];
        observer.observe(plot_div, config);
    }}
    updateTable(active_trace)
    attachDropdownListener();
</script>
"""

html_content = f"""
<!DOCTYPE html>
<html>
<head>`
    <title>Interactive Dashboard</title>
</head>
<body>
    <h1>Interactive Dashboard with Plotly</h1>
    <div id="chart-container">{plot_div}</div>
    <div id="comment-content"></div>
    <style>
  table {{
    margin-left: 64px; /* Khoảng cách lề 2 tab (1 tab = 16px) */
    border-collapse: collapse;
    width: auto; /* Tự động điều chỉnh độ rộng */
    table-layout: auto; /* Tự động điều chỉnh layout */
  }}

  th, td {{
    border: 1px solid black;
    padding: 8px;
    text-align: center;
  }}

  th {{
    background-color: #f2f2f2;
  }}
</style>
</head>
<body>

  <p style="margin-left: 16px;"> <!-- Thêm margin-left cho cả đoạn văn bản chứa bảng -->
    <table>
      <tr>
        <th>Năm</th>
        <th>Số giao dịch dương</th>
        <th>Số giao dịch âm</th>
      </tr>
      <tr>
        <td>2021</td>
        <td id="positive-2021"></td>
        <td id="negative-2021"></td>
      </tr>
      <tr>
        <td>2022</td>
        <td id="positive-2022"></td>
        <td id="negative-2022"></td>
      </tr>
      <tr>
        <td>2023</td>
        <td id="positive-2023"></td>
        <td id="negative-2023"></td>
      </tr>
      <tr>
        <td>2024</td>
        <td id="positive-2024"></td>
        <td id="negative-2024"></td>
      </tr>
    </table>
  </p>
  {js_code}
</body>
</html>
"""



with open("example_axis.html", "w", encoding="utf-8") as f:
    f.write(html_content)
In [ ]:
for in in range(700):
    quant_sim[f'{i}'][0]=quant_sim[f'{i}'][0].explode('DATE')
In [ ]:
with open('QUANT_SIM_TOTAL','rb') as file:
    quant_sim=pk.load(file)
In [ ]:
(pd.to_datetime(return_profit.index[-1])-pd.to_datetime(return_profit.index[0])).days
In [ ]:
pdts(first_day)
In [ ]:
columns = ['Trading_days','Growth_vni','Growth_por','Cagr','Mean', 'Std', 'Win_Loss_Ratio', 'Gain_Pain_Percent', 'Outlier_Win', 'Outlier_Loss', 'Sharpe']
des_portfolio_1 = np.zeros((700, len(columns)))
rf = 0.07/365
periods = 365 
for i in range(700):
    # if i == 521:continue
    sub_porfolio=backtest_699[f'{i}'][0]
    profit_close=sub_porfolio.drop_duplicates('TOTAL_PROFIT',keep='first').iloc[:,[0,-1]].reset_index(drop=True).set_index('DATE')
    return_profit=profit_close.pct_change().fillna(0)
    
    years = (pd.to_datetime(return_profit.index[-1])-pd.to_datetime(return_profit.index[0])).days/periods
    return_profit=return_profit['TOTAL_PROFIT'].values
    
    day_trading = (len(sub_porfolio))
    first_day=sub_porfolio.DATE[0]
    index_first=find_index(df=vnindex,col_name='DATE',obj=pdts(first_day))
    last_day=pdts('2025-08-28')
    index_last=find_index(df=vnindex,col_name='DATE',obj=last_day)
    growth_vni=round(vnindex.iat[index_last,1]/vnindex.iat[index_first,1],4)-1
    growth_por=sub_porfolio.iat[-1,-1]-1
    cagr=np.abs(np.prod(np.add(return_profit,1)))**(1/years) - 1
    
    mean_portfolio=np.round(np.mean(return_profit),5)
    
    std_portfolio=np.round(np.std(return_profit),5)
    
    gains = return_profit[return_profit >= 0]
    losses = return_profit[return_profit < 0]
    gain_pain = np.round(np.mean(gains) / -np.mean(losses), 5)
    
    win_loss_ratio = gains.shape[0]/return_profit.shape[0]
    
    outlier_win=np.round(np.percentile(return_profit,95),5)
    
    outlier_loss=np.round(np.percentile(return_profit,5),5)
    
    sharpe = np.round(np.subtract(mean_portfolio,rf)/std_portfolio,5)
    
    des_portfolio_1[i, :] = [day_trading,growth_vni,growth_por,cagr,mean_portfolio, std_portfolio, win_loss_ratio, gain_pain, outlier_win, outlier_loss, sharpe]

des_portfolio_1 = pd.DataFrame(des_portfolio_1, columns=columns)
# des_portfolio_1.Start_date=pd.to_datetime(des_portfolio_1['Start_date'])
In [ ]:
des_portfolio_1
In [ ]:
des_portfolio_1.loc[:,['Start_date','Trading_days','Growth_vni','Growth_por','Cagr']]
In [ ]:
des_portfolio_1.to_excel('RANDOM_DATA_BACKTEST.xlsx')
In [ ]:
des_portfolio_1.nlargest(200,'Growth_por')
In [ ]:
with open('QUANT_SIM_100','rb') as file:
    quant_100=pk.load(file)
    
with open('QUANT_SIM_200','rb') as file:
    quant_200=pk.load(file)

with open('QUANT_SIM_300','rb') as file:
    quant_300=pk.load(file)

with open('QUANT_SIM_400','rb') as file:
    quant_400=pk.load(file)
    
with open('QUANT_SIM_500','rb') as file:
    quant_500=pk.load(file)

with open('QUANT_SIM_600','rb') as file:
    quant_600=pk.load(file)
In [ ]:
quant_sim_1={**quant_100,**quant_200,**quant_300,**quant_400,**quant_500,**quant_600}
In [ ]:
# 1. Tạo biểu đồ Scatter (dùng px)
scatter_fig = px.scatter(des_portfolio, x='Std', y='Mean', color='Color', title='Mean vs. Std')

# 2. Tạo biểu đồ Histogram (dùng px)
hist_fig = px.histogram(des_portfolio, y='Mean', title='Histogram of Mean', nbins=30)
hist_fig.update_layout(showlegend=False) # Ẩn chú thích

# 3. Tạo biểu đồ Rug (dùng go)
# rug_fig = go.Figure(go.Box(x=des_portfolio['Std'], name='Rug Plot (Std)', marker=dict(color='green')))
# rug_fig.update_layout(showlegend=False) # Ẩn chú thích

# 4. Tạo subplot (1 hàng, 2 cột, với shared_xaxes và shared_yaxes)
fig = make_subplots(rows=2, cols=2,
                    
                    shared_xaxes=True,
                    shared_yaxes=True,
                    vertical_spacing=0.02,
                    horizontal_spacing=0.02
                   )

# 5. Thêm trace vào subplot

# Rug plot
fig.add_trace(go.Box(x=des_portfolio['Std'], name='Rug Std',marker_symbol='line-ns-open',line_color='rgba(255,255,255,0)',
                    fillcolor='rgba(255,255,255,0)',boxpoints='all',jitter=0, hoveron='points',marker_color='green'), row=1, col=1)

# Scatter plot
for trace in scatter_fig.data:
    fig.add_trace(trace, row=2, col=1)

# Histogram
fig.add_trace(hist_fig.data[0], row=2, col=2)


updatemenus=[{'buttons':[{
    'method': 'update',
    'label': 'Mean vs. Std',
    'args': [{'visible': [
       True,
       True,
       True,
       ]},
    {'title': 'Mean vs. Std'}]}],
    'direction': 'down',
    'showactive': True,
    'x': 0.1,
    'xanchor': 'left',
    'y': 1.15,
    'yanchor': 'top'}]
# 6. Cập nhật layout
fig.update_layout(updatemenus=updatemenus, title_text="Interactive Dashboard")
In [ ]:
# 1. Tạo biểu đồ Scatter (dùng px)
des_portfolio['Color'] = np.where(des_portfolio['Mean'] >= 0, 'Positive', 'Negative')
scatter_fig = px.scatter(des_portfolio, x='Std', y='Mean', color='Color', title='Mean vs. Std')

# 2. Tạo biểu đồ Histogram (dùng px)
hist_fig = px.histogram(des_portfolio, y='Mean', title='Histogram of Mean', nbins=30)
# hist_fig.update_layout(showlegend=False) # Ẩn chú thích

# 3. Tạo biểu đồ Rug (dùng go)
# rug_fig = go.Figure(go.Box(x=des_portfolio['Std'], name='Rug Plot (Std)', marker=dict(color='green')))
# rug_fig.update_layout(showlegend=False) # Ẩn chú thích

# 4. Tạo subplot (1 hàng, 2 cột, với shared_xaxes và shared_yaxes)
fig = make_subplots(rows=5, cols=2,
                    shared_xaxes=True,
                    shared_yaxes=True,
                    vertical_spacing=0.02,
                    horizontal_spacing=0.02,
                   )

# 5. Thêm trace vào subplot

# Rug plot
fig.add_trace(go.Box(x=des_portfolio['Std'], name='Rug Std',marker_symbol='line-ns-open',line_color='rgba(255,255,255,0)',
                    fillcolor='rgba(255,255,255,0)',boxpoints='all',jitter=0, hoveron='points',marker_color='green'), 
              row=1, col=1)

# Scatter plot
for trace in scatter_fig.data:
    fig.add_trace(trace, row=2, col=1)

# Histogram
fig.add_trace(hist_fig.data[0], row=2, col=2)




# Tạo biểu đồ Scatter
des_portfolio['Diff_Outlier'] = des_portfolio['Outlier_Win'].sub(abs(des_portfolio['Outlier_Loss']))
des_portfolio['Color_Outlier'] = np.where(des_portfolio['Diff_Outlier'] >= 0, 'Positive', 'Negative')
line_fig = px.scatter(des_portfolio,x = abs(des_portfolio['Outlier_Loss']) , y= 'Diff_Outlier',color='Color_Outlier',
                 title='Outlier Win versus Outlier Loss',
                 )

hist_fig_1 = px.histogram(des_portfolio,x=abs(des_portfolio['Outlier_Loss']),color='Color_Outlier',title='Count')
# 4. Tạo subplot (1 hàng, 2 cột, với shared_xaxes và shared_yaxes)
# fig_1 = make_subplots(rows=2, cols=1,
                    
#                     shared_xaxes=True,
#                     shared_yaxes=False,
#                     vertical_spacing=0.02,
#                     horizontal_spacing=0.02
#                    )
for trace in line_fig.data:
    fig.add_trace(trace, row=3, col=1)
for trace in hist_fig_1.data:
    fig.add_trace(trace, row=4 , col=1)

hist_cagr_fig = px.histogram(des_portfolio, x='Cagr', nbins=30, title='Histogram of CAGR')
for trace in hist_cagr_fig.data:fig.add_trace(trace,row=4,col=2)

# Tạo ma trận tương quan
gain_pain_fig = px.histogram(des_portfolio, x='Gain_Pain_Percent', title='Gain_To_Pain',opacity=0.8)
for trace in gain_pain_fig.data:fig.add_trace(trace,row=5,col=1)
updatemenus = [
    {
        'buttons': [
            {
                'method': 'update',
                'label': 'Histogram of CAGR',
                'args': [
                    {'visible': [False, False, False,False, False, False, False,False,True,False]},
                    {'title': 'Histogram of CAGR',
                    # Vì chỉ có một biểu đồ nên muốn hiển thị toàn bộ nhưng không thể tuy nhiên khi xóa thì biểu đồ hiển thị ở góc trên bên trái ???
                     'xaxis': {'domain': [0,0.001],'showticklabels':False}, # Hiển thị đồ thị rug ở góc trên bên trái
                     'yaxis': {'domain': [0,0.001],'showticklabels':False}, # Hiển thị đồ thị rug ở góc trên bên trái
                     "xaxis3": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis3": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis4": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis4": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis7": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis7": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis5": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis5": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis9": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis9": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis8": {'domain':[0,1]}, # Vị trí ban đầu của B
                     "yaxis8": {'domain':[0,1]},
                     'annotations': []
                     
                    }
                ]
            },
            {
                'method': 'update',
                'label': 'Mean vs. Std',
                'args': [
                    {'visible': [True, True, True,True, False, False, False,False,False,False]},
                    {'title': 'Mean vs. Std',
                     'xaxis': {'domain': [0, 0.5],'showticklabels': False}, # Hiển thị đồ thị rug ở góc trên bên trái
                     'yaxis': {'domain': [0.6, 1],'showticklabels': False}, # Hiển thị đồ thị rug ở góc trên bên trái
                     'xaxis3': {'domain': [0, 0.5]},  
                     'yaxis3': {'domain': [0, 0.5]}, 
                     'xaxis4': {'domain': [0.53, 1],'showticklabels': False}, # Hiển thị đồ thị histogram ở góc dưới bên phải 
                     'yaxis4': {'domain': [0, 0.5],'showticklabels': False}, # Hiển thị đồ thị histogram ở góc dưới bên phải
                     "xaxis5": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis5": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis7": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis7": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis8": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis8": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis9": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis9": {'domain':[0,0.001],'showticklabels':False},
                     'annotations': [
                                        dict(
                                            x=0,  # Vị trí x (giữa domain của subplot 1)
                                            y=0.75,  # Vị trí y (ngay dưới trục x của subplot 1)
                                            text="Rug Plot",  # Tên của subplot
                                            showarrow=False,  # Không hiển thị mũi tên
                                            xref="x1",  # Tham chiếu đến trục x của subplot 1
                                            yref="y",  # Tham chiếu đến toàn bộ figure
                                            yanchor="top" # Căn chỉnh text lên trên
                                                            ),
                                        dict(
                                            x=0,
                                            y=0,
                                            text="Scatter Plot",
                                            showarrow=False,
                                            xref="x",
                                            yref="paper",
                                            yanchor="top"
                                                            ),
                                        dict(
                                            x=1,
                                            y=0,
                                            text="Histogram",
                                            showarrow=False,
                                            xref="paper",
                                            yref="paper",
                                            yanchor="top"
                                                            ),
                         
                                    ]
                     
                    }
                ]
            },
            {
                'method': 'update',
                'label': 'Outlier Win vs. Loss',
                'args': [
                    {'visible': [False, False, False, False, True, True, True,True,False,False]},
                    {'title': 'Outlier Win vs. Loss',
                     "xaxis": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis3": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis3": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis4": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis4": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis7": {'domain':[0,1],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis7": {'domain':[0.55,1],'showticklabels':False},
                     "xaxis5": {'domain':[0,1]}, # Vị trí ban đầu của B
                     "yaxis5": {'domain':[0,0.5]},
                     "xaxis8": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis8": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis9": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis9": {'domain':[0,0.001],'showticklabels':False},
                     'annotations': [
                                        dict(
                                            x=0,  # Vị trí x (giữa domain của subplot 1)
                                            y=0,  # Vị trí y (ngay dưới trục x của subplot 1)
                                            text="Scatter Plot",  # Tên của subplot
                                            showarrow=False,  # Không hiển thị mũi tên
                                            xref="paper",  # Tham chiếu đến trục x của subplot 1
                                            yref="paper",  # Tham chiếu đến toàn bộ figure
                                            yanchor="top" # Căn chỉnh text lên trên
                                                            ),
                                        dict(
                                            x=0,
                                            y=0.55,
                                            text="Histogram Plot",
                                            showarrow=False,
                                            xref="paper",
                                            yref="paper",
                                            yanchor="top"
                                                            ),
                         
                                    ]
                     
                     
                    }
                ]
            },
            {
                'method': 'update',
                'label': 'Pain To Gain',
                'args': [
                    {'visible': [False, False, False, False, False, False, False,False,False,True]},
                    {'title': 'Pain To Gain',
                    # Vì chỉ có một biểu đồ nên muốn hiển thị toàn bộ nhưng không thể tuy nhiên khi xóa thì biểu đồ hiển thị ở góc trên bên trái ???
                     'xaxis': {'domain': [0,0.001],'showticklabels':False}, # Hiển thị đồ thị rug ở góc trên bên trái
                     'yaxis': {'domain': [0,0.001],'showticklabels':False}, # Hiển thị đồ thị rug ở góc trên bên trái
                     "xaxis3": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis3": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis4": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis4": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis7": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis7": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis5": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis5": {'domain':[0,0.001],'showticklabels':False},
                     "xaxis9": {'domain':[0,1]}, # Vị trí ban đầu của B
                     "yaxis9": {'domain':[0,1]},
                     "xaxis8": {'domain':[0,0.001],'showticklabels':False}, # Vị trí ban đầu của B
                     "yaxis8": {'domain':[0,0.001],'showticklabels':False},
                     'annotations': []
                     
                    }
                ]
            }
            
        ],
        'direction': 'down',
        'showactive': True,
        'x': 0.1,
        'xanchor': 'left',
        'y': 1.15,
        'yanchor': 'top'
    }
]
for i in range(8):
    fig.data[i].visible = False
fig.data[9].visible = False
# 6. Cập nhật layout
fig.update_layout(updatemenus=updatemenus, 
                  title_text="Interactive Dashboard",
                  barmode='overlay',
                  scattermode='overlay',
#                   margin=dict(l=0, r=0, b=0, t=0),
                  xaxis1 = dict(domain = [0, 0.001],showticklabels= False), # Hiển thị đồ thị rug ở góc trên bên trái
                  yaxis1 = dict(domain = [0, 0.001],showticklabels= False), # Hiển thị đồ thị rug ở góc trên bên trái
                  xaxis3 = dict(domain = [0, 0.001],showticklabels= False), 
                  yaxis3 = dict(domain = [0, 0.001],showticklabels= False), 
                  xaxis4 = dict(domain = [0, 0.001],showticklabels= False), # Hiển thị đồ thị histogram ở góc dưới bên phải 
                  yaxis4 = dict(domain = [0, 0.001],showticklabels= False), # Hiển thị đồ thị histogram ở góc dưới bên phải
                  # Điều chỉnh margin cho subplot (1,1) để nó chiếm toàn bộ không gian
                  xaxis5 = dict(domain=[0.001, 0.001],showticklabels= False),
                  yaxis5 = dict(domain=[0.001, 0.001],showticklabels= False),
                  xaxis7 = dict(domain=[0, 0.001],showticklabels= False), # Vị trí ban đầu của B
                  yaxis7 = dict(domain=[0, 0.001],showticklabels= False),
                  xaxis8 = dict(domain=[0, 1],showticklabels= True),
                  yaxis8 = dict(domain=[0, 1],showticklabels= False),
                  xaxis9 = dict(domain=[0, 0.001],showticklabels= False), # Vị trí ban đầu của B
                  yaxis9 = dict(domain=[0, 0.001],showticklabels= False),
                 )
plot_div = plot(fig, output_type='div', include_plotlyjs=True)
js_code = """
<script>
    var comments = {
        'Histogram of CAGR': 'Biểu đồ trên là Histogram của CAGR (hay còn gọi là phân phối lợi nhuận kép hằng năm).<br><ul><li> Đây là biểu đồ quan trọng nhằm đánh giá lợi nhuận tăng trưởng âm/dương trong một năm giao dịch.</li><li>Trong 600 mô phỏng thì chỉ có 102 mô phỏng cho ra kết quả tăng trưởng âm hằng năm. Và mức lỗ cao nhất từ 8-10% hằng năm chỉ có 4/600 mô phỏng</li><li>Phần còn lại là tăng trưởng dương. Trong đó mức tăng trưởng tối thiểu bằng lãi suất ngân hàng (6%/năm) chiếm gần 350/600 mô phỏng</li></ul>',
        'Mean vs. Std': 'Ba biểu đồ trên mô tả trung bình và độ lệch của lợi nhuận thay đổi khi kết thúc ít nhất 1 giao dịch.<br><ul><li> Biểu đồ phân tán (scatter plot) nằm ở giữa là biểu đồ mô tả cả giá trị trung bình và độ lệch của lợi nhuận. Nhìn tổng quan có thể thấy cả về độ lớn lẫn số lượng thì trung bình danh mục lợi nhuận dương áp đảo.</li><li>Biểu đồ đường rug (rug plot) ở phía trên cho thấy độ lệch của trung bình lợi nhuận danh mục. Độ tập trung nhiều từ 2.2%-3.2% trong tổng 600 mô phỏng</li><li>Biểu đồ phân phối (histogram) của lợi nhuận trung bình cho thấy chỉ 75/600 mô phỏng cho ra mức lợi nhuận trung bình danh mục âm.</li></ul>',
        'Outlier Win vs. Loss': 'Cả hai biểu đồ trên thể hiện các chênh lệch giữa lợi nhuận cao bất thường và lỗ tồi tệ trong 600 mô phỏng ngẫu nhiên.<br><ol><li>Biểu đồ phân tán trực quan cho thấy sự áp đảo của các lợi nhuận cao đối với các khoản lỗ cao</li><li>Biểu đồ phân phối biểu diễn các mức lỗ cao trong 600 mô phỏng. Màu đỏ thể hiện tại đó, các mức lỗ cao lấn áp lợi nhuận.Bên cạnh đó, các mức lỗ cao chỉ từ 2-4% cho thấy sự quản trị rủi ro tốt.</li></ol>',
        'Pain To Gain': 'Biểu đồ phân phối thể hiện lợi nhuận trung bình chia cho lỗ trung bình của từng danh mục.<br><ol><li>Biểu đồ cho thấy không có giá trị nào nhỏ hơn 1, tức là trong 600 mô phỏng ngẫu nhiên lợi nhuận trung bình luôn lớn hơn lỗ trung bình. Đây là một chỉ báo tốt cho thấy hiệu quả của chiến lược đầu tư.</li><li>Hơn nữa, khoảng giá trị tập trung nhiều nhất là tại 1.4-1.9 cho thấy sự vượt trội của lợi nhuận từ 40-90%. Và đó là còn chưa kể rất nhiều giá trị khác cao hơn đáng kể nằm ở mức phân vị 75-100%.</li></ol>',
    };
    var plot_div = document.getElementsByClassName('plotly-graph-div js-plotly-plot')
    var comment_div = document.getElementById('comment-content');
    var active_trace = 'Histogram of CAGR';  // Ban đầu hiển thị comment của biểu đồ đầu tiên

    function updateComment(traceName) {
        comment_div.innerHTML = comments[traceName];
    }

    // Lắng nghe sự kiện mousedown trên các nút bấm của dropdown menu
    function attachDropdownListener() {
        // Create a new MutationObserver instance
        var observer = new MutationObserver(function (mutations) {
            // Check for the buttons in the DOM
            var buttons = document.querySelectorAll("g.updatemenu-dropdown-button-group .updatemenu-dropdown-button");
            if (buttons.length > 0) {
                buttons.forEach(function (button) {
                    if (!button.hasAttribute('listener-attached')) { // Prevent duplicate listeners
                        button.addEventListener('mousedown', function (event) {
                            var clickedButton = event.currentTarget;
                            // Retrieve related text
                            var relatedText = clickedButton.querySelector("text");
                            console.log("🚀 ~ relatedText:", relatedText)
                            if (relatedText && relatedText.textContent) {
                                var traceName = relatedText.textContent.trim();
                                updateComment(traceName); // Update the comment with the correct text
                            }
                        });

                        // Mark the button as having a listener
                        button.setAttribute('listener-attached', 'true');
                    }
                });
            }
        });


        // Configuration for the observer
        var config = {
            childList: true,
            subtree: true
        };

        // Start observing the target node for configured mutations
        var plot_div = document.getElementsByClassName('plotly-graph-div js-plotly-plot')[0];
        observer.observe(plot_div, config);
    }
    updateComment(active_trace)
    attachDropdownListener();
</script>
"""

html_content = f"""
<!DOCTYPE html>
<html>
<head>
    <title>Interactive Dashboard</title>
</head>
<body>
    <h1>Interactive Dashboard with Plotly</h1>
    <div id="chart-container">{plot_div}</div>
    <div id="comment-content"></div>
    {js_code}
</body>
</html>
"""



with open("dashboard_with_comments.html", "w", encoding="utf-8") as f:
    f.write(html_content)